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