Предположим — у вас имеется функция, которая передаёт 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)


  1. beefdeadbeef
    24.06.2025 08:34

    Ха, люблю такое, спасибо.


  1. a-tk
    24.06.2025 08:34

    На x86_64 запись 32-битного регистра приводит к обнулению старших битов соответствующего регистра: http://x86asm.net/articles/x86-64-tour-of-intel-manuals/

    Поэтому неверно, что в старших битах остаётся мусор. А поскольку в конвенциях вызова регистр для аргумента и регистр для результат разные, то мы в любом случае получаем обнуление старших битов.