Предположим — у вас имеется функция, которая передаёт 32-битное значение другой функции, принимающей 64-битные значения. Вам совершенно неважно то, что попадёт в 32 старших бита, так как это значение функция, принимающая его, напрямую не обрабатывает. Его просто передают функции обратного вызова, которая обрезает его, преобразуя в 32-битное значение. При этом, по некоей причине, вас беспокоит влияние на производительность той единственной инструкции, которую компилятор обычно генерирует для расширения 32-битных значений до 64-битных.
Первое, что я по этому поводу подумал, выглядело так: «Да зачем об этом беспокоиться, если пока ничего особенного не произошло». Подозреваю, что одна единственная инструкция не превратится в узкое место некоей программы.
Но, несмотря на это, я, просто из интереса, решил попробовать решить эту хитрую задачку.
Я решил использовать встроенный ассемблер gcc/clang и написать код, который сообщает системе: «Я могу создать 64-битное значение из 32-битного, не выполнив ни одной инструкции».

int64_t int32_to_64_garbage(int32_t i32)
{
int64_t i64;
__asm__("" : // ничего не делать
"=r"(i64) : // формирует результат в регистре
"0"(i32)); // из этих входных данных
return i64;
}
Первый аргумент директивы __asm__
— это код, который нужно сгенерировать. Мы передаём директиве пустую строку, поэтому получается, что никакого кода она не создаёт! Все необходимые нам эффекты реализуются посредством объявлений входных и выходных параметров.
Дальше идут выходные значения, в нашем случае — всего одно значение. Конструкция "=r"(i64)
указывает на то, что встроенный ассемблер поместит перезаписываемое (=
) значение i64
в регистр r
, выбираемый компилятором. К этому регистру встроенный ассемблер будет обращаться как к %0
. (Выходные параметры нумеруются, начиная с нуля).
И наконец — у нас есть входные параметры, которые, опять же, представлены единственным значением. Смысл конструкции "0"(i32)
в том, что входные данные нужно поместить туда же, куда и выходные данные номер 0.
Всё, что нам нужно, делается с помощью ограничителей входных и выходных параметров. Тут, на самом деле, нет никакого кода. Мы, фактически, говорим компилятору: «Помести i32
в регистр, а потом закрой глаза. Когда их откроешь — i64
будет в том же самом регистре».
Запуск gcc с уровнем оптимизации 3 привёл к тому, что значение было полностью убрано из кода.
void somewhere(int64_t);
void sample1(int32_t v)
{
somewhere(v);
}
void sample2(int32_t v)
{
somewhere(int32_to_64_garbage(v));
}
Вот что получилось:
// x86-64
sample1(int):
movsx rdi, edi
jmp somewhere(long)
sample2(int):
jmp somewhere(long)
// arm32
sample1(int):
asrs r1, r0, #31
b somewhere(long long)
sample2(int):
b somewhere(long long)
// arm64
sample1(int):
sxtw x0, w0
b somewhere(long)
sample2(int):
b somewhere(long)
Первая версия функции содержит, в явном виде, инструкцию расширения знака, стоящую до хвостового вызова. Во второй версии функции просто делается хвостовой вызов, при этом используется абсолютно всё что угодно, присутствующее в верхних 32 битах регистра rdi
.
Ещё один компилятор, который поддерживает расширенный синтаксис встроенного ассемблера gcc — это icc. Этот фокус, похоже, работает и здесь:
// x86-64
sample1(int):
movsxd rdi, edi
jmp somewhere(long)
sample2(int):
jmp somewhere(long)
Компилятор clang тоже поддерживает расширенный синтаксис встроенного ассемблера. Он, правда, не только вставляет в код команду преобразования, но ещё и теряет хвостовой вызов.
// x86-64
sample1(int):
movsxd edi, edi
jmp somewhere(long)@PLT
sample2(int):
push rax
mov edi, edi
call somewhere(long)@PLT
pop rax
ret
// arm32
sample1(int):
asr r1, r0, #31
b somewhere(long long)
sample2(int):
push {r11, lr}
sub sp, sp, #8
mov r1, #0
bl somewhere(long long)
add sp, sp, #8
pop {r11, pc}
// arm64
sample1(int):
sxtw x0, w0
b somewhere(long)
sample2(int):
sub sp, sp, #32
stp x29, x30, [sp, #16]
add x29, sp, #16
mov w0, w0
bl somewhere(long)
ldp x29, x30, [sp, #16]
add sp, sp, #32
ret
Обновление: похоже, что в текущей (на момент написания статьи) версии clang хвостовой вызов восстанавливается. Но компилятор, при этом, создаёт код, который выполняет преобразование 32-битного значения в 64-битное без расширения знака. В результате разные варианты функции, по сути, нагружают систему одинаково.
// x86-64
sample1(int):
movsxd edi, edi
jmp somewhere(long)@PLT
sample2(int):
mov edi, edi
jmp somewhere(long)@PLT
// arm32
sample1(int):
asr r1, r0, #31
b somewhere(long long)
sample2(int):
mov r1, #0
b somewhere(long long)
// arm64
sample1(int):
sxtw x0, w0
b somewhere(long)
sample2(int):
mov w0, w0
b somewhere(long)
Компилятор Microsoft Visual C++ не поддерживает расширенный синтаксис встроенного ассемблера gcc. Поэтому на msvc мы этот код проверить не можем.
Этот приём совершенно не работает в msvc. Его применение не даёт нам никаких преимуществ в clang. Поэтому я включил бы подобную оптимизацию только если бы компилировал код с помощью gcc или icc, а при использовании других компиляторов просто смирился бы с лишней инструкцией.
(А если честно — я бы этим вообще нигде не пользовался, за исключением случаев абсолютной необходимости. Всё это — просто так называемый «code golfing», когда, например — на соревнованиях, программисты пытаются решить задачу, прибегнув к коду минимального объёма.)
О, а приходите к нам работать? ? ?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Комментарии (2)
a-tk
24.06.2025 08:34На x86_64 запись 32-битного регистра приводит к обнулению старших битов соответствующего регистра: http://x86asm.net/articles/x86-64-tour-of-intel-manuals/
Поэтому неверно, что в старших битах остаётся мусор. А поскольку в конвенциях вызова регистр для аргумента и регистр для результат разные, то мы в любом случае получаем обнуление старших битов.
beefdeadbeef
Ха, люблю такое, спасибо.