Разрешен ли мусор в старших битах регистров параметров и возвращаемых значений в x86-64 SysV ABI?
SysV ABI x86-64 указывает, среди прочего, как параметры функции передаются в регистрах (первый аргумент вrdi
, затемrsi
и так далее) и как целочисленные возвращаемые значения передаются обратно (вrax
а потомrdx
для действительно больших ценностей).
Однако я не могу найти то, какими должны быть старшие биты регистров параметров или возвращаемых значений при передаче типов, меньших 64-битных.
Например, для следующей функции:
void foo(unsigned x, unsigned y);
...x
будет передано вrdi
а такжеy
вrsi
, но они только 32-битные. Делать высокие 32-битныеrdi
а такжеrsi
должен быть ноль? Интуитивно я бы предположил, что да, носгенерированный код по всем gcc, clang и icc имеет определенныеmov
инструкции в начале обнулить старшие биты, поэтому кажется, что компиляторы предполагают обратное.
Аналогично, компиляторы, похоже, предполагают, что старшие биты возвращаемого значенияrax
может иметь биты мусора, если возвращаемое значение меньше 64 бит. Например, циклы в следующем коде:
unsigned gives32();
unsigned short gives16();
long sum32_64() {
long total = 0;
for (int i=1000; i--; ) {
total += gives32();
}
return total;
}
long sum16_64() {
long total = 0;
for (int i=1000; i--; ) {
total += gives16();
}
return total;
}
...компилировать к следующему вclang
(и другие компиляторы похожи):
sum32_64():
...
.LBB0_1:
call gives32()
mov eax, eax
add rbx, rax
inc ebp
jne .LBB0_1
sum16_64():
...
.LBB1_1:
call gives16()
movzx eax, ax
add rbx, rax
inc ebp
jne .LBB1_1
Обратите вниманиеmov eax, eax
после вызова, возвращающего 32 бита, иmovzx eax, ax
после 16-битного вызова - оба обнуляют старшие 32 или 48 бит соответственно. Таким образом, это поведение имеет определенную стоимость - тот же цикл, работающий с 64-битным возвращаемым значением, пропускает эту инструкцию.
Я прочиталx86-64 документ System V ABI довольно осторожно, но я не мог найти, документировано ли это поведение в стандарте.
Каковы преимущества такого решения? Мне кажется, есть очевидные затраты:
Стоимость параметраЗатраты накладываются на реализацию вызываемого при работе со значениями параметров. и в функциях при работе с параметрами. Конечно, часто эта стоимость равна нулю, потому что функция может эффективно игнорировать старшие биты, или обнуление происходит бесплатно, поскольку могут использоваться инструкции размера 32-битного операнда, которые неявно обнуляют старшие биты.
Однако затраты часто бывают весьма реальными в случаях, когда функции принимают 32-битные аргументы и выполняют некоторую математику, которая может выиграть от 64-битной математики. приниматьэта функция например:
uint32_t average(uint32_t a, uint32_t b) {
return ((uint64_t)a + b) >> 2;
}
Прямое использование 64-битной математики для вычисления функции, которая в противном случае должна была бы аккуратно справляться с переполнением (возможность преобразования многих 32-битных функций таким способом часто является незамеченным преимуществом 64-битных архитектур). Это компилируется в:
average(unsigned int, unsigned int):
mov edi, edi
mov eax, esi
add rax, rdi
shr rax, 2
ret
Полностью 2 из 4 инструкций (игнорируяret
) нужны только для обнуления старших бит. На практике это может быть дешево с устранением mov, но все же это кажется большой ценой.
С другой стороны, я не могу увидеть аналогичную соответствующую стоимость для вызывающих, если бы ABI указывал, что старшие биты равны нулю. Так какrdi
а такжеrsi
и другие регистры передачи параметровцарапина (т. е. может быть перезаписано вызывающей стороной), у вас есть только пара сценариев (мы рассмотримrdi
, но замените его параметром reg на ваш выбор):
Значение, переданное функции вrdi
мертв (не требуется) в коде после вызова. В этом случае, какая инструкция была назначена последнейrdi
просто должен назначитьedi
вместо. Это не только бесплатно, но часто на один байт меньше, если вы избегаете префикса REX.
Значение, переданное функции вrdi
является нужен после функции. В этом случае, так какrdi
сохраняется для вызывающего абонента, вызывающий должен сделатьmov
значения в регистр, сохраненный вызываемым пользователем в любом случае. Вы можете вообще организовать это так, чтобы ценностьначинается в сохраненном регистре собеседника (скажемrbx
) и затем перемещается вedi
лайкmov edi, ebx
так что ничего не стоит.
Я не вижу много сценариев, когда обнуление обходится клиенту очень дорого. Некоторые примеры были бы, если 64-битная математика необходима в последней инструкции, которая присвоилаrdi
, Это кажется довольно редким, хотя.
Здесь решение кажется более нейтральным. После того, как вызываемые абоненты убрали мусор, у него есть определенный код (иногда вы видитеmov eax, eax
инструкции сделать это), но если разрешен мусор, расходы переносятся на вызываемого абонента. В целом, кажется более вероятным, что вызывающая сторона может очистить нежелательную память бесплатно, поэтому разрешение мусора не кажется в целом вредным для производительности.
Я предполагаю, что один интересный вариант использования этого поведения состоит в том, что функции с различными размерами могут иметь одинаковую реализацию. Например, все следующие функции:
short sums(short x, short y) {
return x + y;
}
int sumi(int x, int y) {
return x + y;
}
long suml(long x, long y) {
return x + y;
}
Может фактически использовать одну и ту же реализацию1:
sum:
lea rax, [rdi+rsi]
ret
1 Является ли такое свертывание на самом делепозволил для функций, чей адрес взят очень многооткрыт для обсуждения.