Как следует определять типы [u] int_fastN_t для x86_64, с ABI для x32 или без него?

x32 ABI определяет, среди прочего, 32-разрядные указатели для кода, созданного для архитектуры x86_64. . Он сочетает в себе преимущества архитектуры x86_64 (включая 64-битные регистры ЦП) с уменьшенными накладными расходами 32-битных указателей.

Заголовок <stdint.h> определяет определения типов int_fast8_t, int_fast16_t, int_fast32_t и int_fast64_t (и соответствующие типы без знака uint_fast8_t и др.), Каждый из которых:

целочисленный тип, с которым обычно работать быстрее всего среди всех целочисленных типов, которые имеют как минимум заданную ширину

со сноской:

Не гарантируется, что указанный тип будет самым быстрым для всех целей; если у реализации нет явных оснований для выбора одного типа над другим, она просто выберет некоторый целочисленный тип, удовлетворяющий требованиям подписи и ширины.

(Цитата из N1570 C11 draft.)

Вопрос в том, как следует определять типы [u]int_fast16_t и [u]int_fast32_t для архитектуры x86_64, с ABI x32 или без него? Есть ли документ x32, в котором указаны эти типы? Должны ли они быть совместимы с 32-битными определениями x86 (оба 32-битные) или, поскольку x32 имеет доступ к 64-битным регистрам ЦП, должны ли они быть одинакового размера с x32 ABI или без него? (Обратите внимание, что x86_64 имеет 64-разрядные регистры независимо от того, используется ли x32 ABI или нет.)

Вот тестовая программа (которая зависит от специфичного для gcc макроса __x86_64__):

#include <stdio.h>
#include <stdint.h>
#include <limits.h>

int main(void) {
#if defined __x86_64__ && SIZE_MAX == 0xFFFFFFFF
    puts("This is x86_64 with the x32 ABI");
#elif defined __x86_64__ && SIZE_MAX > 0xFFFFFFFF
    puts("This is x86_64 without the x32 ABI");
#else
    puts("This is not x86_64");
#endif
    printf("uint_fast8_t  is %2zu bits\n", CHAR_BIT * sizeof (uint_fast8_t));
    printf("uint_fast16_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast16_t));
    printf("uint_fast32_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast32_t));
    printf("uint_fast64_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast64_t));
}

Когда я компилирую его с gcc -m64, на выходе получается:

This is x86_64 without the x32 ABI
uint_fast8_t  is  8 bits
uint_fast16_t is 64 bits
uint_fast32_t is 64 bits
uint_fast64_t is 64 bits

Когда я компилирую его с gcc -mx32, на выходе получается:

This is x86_64 with the x32 ABI
uint_fast8_t  is  8 bits
uint_fast16_t is 32 bits
uint_fast32_t is 32 bits
uint_fast64_t is 64 bits

(который, помимо первой строки, совпадает с выводом с gcc -m32, который генерирует 32-битный код x86).

Это ошибка в glibc (которая определяет заголовок <stdint.h>) или это соответствует некоторому требованию x32 ABI? Ссылки на типы [u]int_fastN_t отсутствуют в x32 ABI документ или x86_64 ABI-документ, но может быть что-то еще что определяет это.

Можно утверждать, что типы fast16 и fast32 должны быть 64-битными с x32 или с x32, поскольку доступны 64-битные регистры; будет ли это иметь больше смысла, чем нынешнее поведение?

(Я существенно отредактировал исходный вопрос, который касался только x32 ABI. Теперь вопрос касается x86_64 с x32 или без него.)


person Keith Thompson    schedule 30.04.2016    source источник
comment
Почему это ошибка в glibc?   -  person Ross Ridge    schedule 01.05.2016
comment
@RossRidge: Если вы считаете, что <stdint.h> предоставляется glibc, а не gcc, вы правы; Я обновил вопрос. Если вы говорите, что это не ошибка, мне было бы интересно ваше объяснение. Поскольку в системе 64-битные регистры, int64_t должен быть быстрее, чем int32_t, поэтому int_fast32_t должен быть 64-битным, как и в x86_64.   -  person Keith Thompson    schedule 01.05.2016
comment
Мне интересно сначала услышать ваше рациональное мнение. Почему наличие 64-битных регистров делает int64_t быстрее, чем int32_t при работе со значениями, которым требуется только 32 бита?   -  person Ross Ridge    schedule 01.05.2016
comment
@RossRidge: (1) Например, копирование 64-битного регистра в или из 64-битного объекта памяти может быть быстрее, чем копирование 64-битного регистра в или из 32-битного объекта памяти (нет необходимости в маскировке или знаке). расширение). (2) Для x86_64, отличного от x32, gcc уже делает [u]int_fast16_t и [u]int_fast32_t 64-битными. Какое бы ни было обоснование этого решения, оно также применимо к x32, если я чего-то не упускаю.   -  person Keith Thompson    schedule 01.05.2016
comment
Для копирования 64-битных значений нужен более длинный префикс REX. Копирование 32-битных значений всегда обнуляет верхние 32 бита, поэтому оно выполняется одинаково быстро или быстрее из-за меньшего места в I-кэше.   -  person phuclv    schedule 01.05.2016
comment
@ LưuVĩnhPhúc: Итак, должен ли gcc сделать [u]int32_t 32 бита на x86_64? В настоящее время они имеют разные размеры на x32 и x86_64; есть ли веские причины отличаться от них?   -  person Keith Thompson    schedule 01.05.2016
comment
@KeithThompson: Я могу только думать, что причина, по которой x86_64 использует 64 бита для всего, заключается в выравнивании: наличие всех типов 64-бит делает идеальный мир с 8-байтовым выравниванием. В x32, поскольку указатели 32-битные, идеальное выравнивание уже нарушено, поэтому наличие int_fast16_t с 64-битными уже не так привлекательно.   -  person rodrigo    schedule 01.05.2016
comment
@rodrigo: Но 4-байтовые целые числа (включая int в большинстве реализаций) выравниваются по 4 байтам, а не по 8.   -  person Keith Thompson    schedule 01.05.2016


Ответы (3)


Вообще говоря, можно ожидать, что 32-разрядные целые типы будут немного быстрее 64-разрядных целочисленных типов на процессорах x86-64. Отчасти потому, что они используют меньше памяти, но также потому, что 64-битные инструкции требуют дополнительного байта префикса по сравнению с их 32-битными аналогами. 32-битная команда деления значительно быстрее 64-битной, но в остальном задержки выполнения команд такие же.

Обычно нет необходимости расширять 32-битные при загрузке их в 64-битные регистры. Хотя в этом случае ЦП автоматически расширяет значения нулями, обычно это дает только преимущество, поскольку позволяет избежать частичных остановок регистров. То, что загружается в верхнюю часть регистра, менее важно, чем то, что изменяется весь регистр. Содержимое верхней части регистра не имеет значения, потому что, когда они используются для хранения 32-битных типов, они обычно используются только с 32-битными инструкциями, которые работают только с нижней 32-битной частью регистра.

Несоответствие между размерами типов int_fast32_t при использовании ABI x32 и x86-64, вероятно, лучше всего оправдано тем фактом, что указатели имеют ширину 64 бита. Всякий раз, когда к указателю добавляется 32-битное целое число, его необходимо расширять, что делает это более вероятным при использовании x86-64 ABI.

Еще один фактор, который следует учитывать, заключается в том, что весь смысл x32 ABI заключается в повышении производительности за счет использования меньших типов. Любое приложение, которое выигрывает от меньшего размера указателей и связанных типов, должно также выиграть от int_fast32_t меньшего размера.

person Ross Ridge    schedule 01.05.2016
comment
Вопрос специфичен для x32 ABI, но теперь я думаю, что вопрос о правильных размерах типов [u]int_fastN_t для x86_64 с и без x32 будет лучшим вопросом. Поскольку вы опубликовали ответ на вопрос в его нынешнем виде, было бы несправедливо его менять. Вы бы возразили, если бы я сделал такое изменение? - person Keith Thompson; 01.05.2016
comment
@KeithThompson Конечно, это не похоже на то, чтобы изменить мой ответ. - person Ross Ridge; 01.05.2016

Я скомпилировал следующий пример кода, чтобы проверить сгенерированный код на простую сумму с разными целочисленными типами:

#include <stdint.h>

typedef int16_t INT;
//typedef int32_t INT;
//typedef int64_t INT;

INT foo()
{
    volatile INT a = 1, b = 2;
    return a + b;
}

А затем я разобрал код, сгенерированный для каждого из целочисленных типов. Команда компиляции gcc -Ofast -mx32 -c test.c. Обратите внимание, что в полном 64-битном режиме сгенерированный код будет почти таким же, потому что в моем коде нет указателей (только %rsp вместо %esp).

С int16_t он излучает:

00000000 <foo>:
   0:   b8 01 00 00 00          mov    $0x1,%eax
   5:   ba 02 00 00 00          mov    $0x2,%edx
   a:   67 66 89 44 24 fc       mov    %ax,-0x4(%esp)
  10:   67 66 89 54 24 fe       mov    %dx,-0x2(%esp)
  16:   67 0f b7 54 24 fc       movzwl -0x4(%esp),%edx
  1c:   67 0f b7 44 24 fe       movzwl -0x2(%esp),%eax
  22:   01 d0                   add    %edx,%eax
  24:   c3                      retq   

С int32_t:

00000000 <foo>:
   0:   67 c7 44 24 f8 01 00 00 00  movl   $0x1,-0x8(%esp)
   9:   67 c7 44 24 fc 02 00 00 00  movl   $0x2,-0x4(%esp)
  12:   67 8b 54 24 f8              mov    -0x8(%esp),%edx
  17:   67 8b 44 24 fc              mov    -0x4(%esp),%eax
  1c:   01 d0                       add    %edx,%eax
  1e:   c3                          retq   

И с int64_t:

00000000 <foo>:
   0:   67 48 c7 44 24 f0 01 00 00 00   movq   $0x1,-0x10(%esp)
   a:   67 48 c7 44 24 f8 02 00 00 00   movq   $0x2,-0x8(%esp)
  14:   67 48 8b 54 24 f0               mov    -0x10(%esp),%rdx
  1a:   67 48 8b 44 24 f8               mov    -0x8(%esp),%rax
  20:   48 01 d0                        add    %rdx,%rax
  23:   c3                              retq   

Я не утверждаю, что точно знаю, почему компилятор сгенерировал именно этот код (может быть, ключевое слово volatile в сочетании с целочисленным типом не регистрового размера - не лучший выбор?). Но из этого сгенерированного кода мы можем сделать следующие выводы:

  1. Самый медленный тип - int16_t. Для перемещения значений требуются дополнительные инструкции.
  2. Самый быстрый тип - int32_t. Хотя 32-разрядная и 64-разрядная версии имеют одинаковое количество инструкций, 32-разрядный код короче в байтах, поэтому он будет более удобен для кеширования, а значит, быстрее.

Итак, естественным выбором для быстрых типов будет:

  1. Для int_fast16_t выберите int32_t.
  2. Для int_fast32_t выберите int32_t.
  3. Для int_fast64_t выберите int64_t (что еще).
person rodrigo    schedule 30.04.2016
comment
Вопрос специфичен для x32 ABI, но теперь я думаю, что вопрос о правильных размерах типов [u]int_fastN_t для x86_64 с и без x32 будет лучшим вопросом. Поскольку вы опубликовали ответ на вопрос в его нынешнем виде, было бы несправедливо его менять. Вы бы возразили, если бы я сделал такое изменение? - person Keith Thompson; 01.05.2016
comment
@KeithThompson: Я не понимаю, что вы имеете в виду под x86_64 с x32 и без него. x86_64 и x32 - разные архитектуры, хотя обе работают на 64-битных процессорах. Но в любом случае я бы ожидал, что целочисленные типы будут одинаковыми, поскольку отличается только размер указателя. - person rodrigo; 01.05.2016
comment
@KeithThompson: Я только что проверил и ошибся. Все эти три быстрых целых числа имеют длину 64 бита в x86_64, что отличается от x32. Может разница связана с выравниванием памяти по умолчанию? Но боюсь, что мой ответ, как он есть, не относится к вопросу x86_64. - person rodrigo; 01.05.2016
comment
Не возражаете, если я обновлю вопрос? Я ненавижу, когда другие задающие вопросы существенно меняют свои вопросы после публикации ответов, и я не хочу делать это без вашего одобрения, но я думаю, что покрытие x86_64 с x32 и без него улучшило бы вопрос. - person Keith Thompson; 01.05.2016
comment
@KeithThompson: О, я совсем не против! На самом деле я думаю, что в любом случае могу ошибаться в этом ответе. Я думаю, знаю, что мало кого интересуют типы *int_fast* и мало кого интересует архитектура x32, поэтому никого не волновали обе вещи одновременно. Может быть, на ваш улучшенный вопрос будет реальный ответ. - person rodrigo; 01.05.2016
comment
x32 - это ABI ILP32 для x86-64. Это та же архитектура, что и обычный -m64 LP64 x86-64 System V ABI для x86-64. x32 даже определен в главе того же документа x86-64 System V ABI. Ваш ответ показывает, как int_fastN_t в настоящее время определены на -mx32, но ничего не говорит о том, хороший это выбор или нет. (Или тот факт, что int_fast32_t на -m64 является 64-битным, что фактически замедляет деление на нем и даже умножение на некоторых процессорах.) - person Peter Cordes; 06.01.2020
comment
@PeterCordes даже int_fast16_t является 64-битным на -m64, что меня больше всего удивило - person phuclv; 07.01.2020
comment
@phuclv: если вы собираетесь сделать int_fast32_t 64-битным типом, было бы непоследовательно делать int_fast16_t уже. Вы могли бы возразить, что он с меньшей вероятностью будет использоваться в качестве индекса массива, устраняя это оправдание для расширения до 64-разрядного. Ясно, что не используйте их для больших массивов (или в структурах, где им может потребоваться дополнительное заполнение) из-за подобных реализаций. - person Peter Cordes; 07.01.2020

Жесткий. Давайте просто возьмем int_fast8_t. Если разработчик использует большой массив для хранения множества 8-битных целых чисел со знаком, тогда int8_t будет самым быстрым из-за кеширования. Я бы сказал, что использование больших массивов int_fast8_t, вероятно, плохая идея.

Вам нужно будет взять большую кодовую базу и систематически заменять int8_t, подписанные символы и простой char, если он подписан с помощью int_fast8_t. Затем протестируйте код, используя различные определения типов для int_fast8_t, и измерьте, что является самым быстрым.

Обратите внимание, что поведение undefined изменится. Например, присвоение 255 даст результат -1, если тип int8_t, и 255 в противном случае.

person gnasher729    schedule 30.04.2016
comment
Я не понимаю, как это отвечает на вопрос. - person Keith Thompson; 01.05.2016
comment
int_fast8_t и uint_fast8_t - 8 бит под gcc; Я спрашивал о [u]int16_t и [u]int32_t. - person Keith Thompson; 01.05.2016
comment
Мне также любопытно, что вы имели в виду под жестким. - person Keith Thompson; 01.05.2016