Глупые фокусы: преобразование 32-битного значения в 64-битное, когда неважен мусор в старших битах

от автора

Предположим — у вас имеется функция, которая передаёт 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 разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде


ссылка на оригинал статьи https://habr.com/ru/articles/921266/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *