Вот реальный пример: умножение с фиксированной запятой на старых компиляторах.
Они не только пригодятся на устройствах без плавающей запятой, они сияют, когда дело доходит до точности, поскольку они дают 32 бита точности с предсказуемой ошибкой (у плавающего есть только 23 бита, и труднее предсказать потерю точности). т. е. равномерная абсолютная точность по всему диапазону вместо почти одинаковой относительной точности ( float
).
Современные компиляторы прекрасно оптимизируют этот пример с фиксированной запятой, поэтому для более современных примеров, которые все еще нуждаются в специфичном для компилятора коде, см.
- Получение большей части 64-битного целочисленного умножения : портативная версия, использующая
uint64_t
32x32 => 64-битное умножение, не может быть оптимизирована на 64-битном процессоре, поэтому вам нужны встроенные или __int128
эффективный код на 64-битных системах.
- _umul128 в Windows 32 бит : MSVC не всегда хорошо справляется с умножением 32-битных целых чисел, приведенных к 64, поэтому встроенные функции очень помогли.
C не имеет оператора полного умножения (2N-битный результат из N-битных входов). Обычный способ выразить это в C - привести входные данные к более широкому типу и надеяться, что компилятор распознает, что старшие биты входных данных не интересны:
// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
Проблема с этим кодом заключается в том, что мы делаем то, что не может быть прямо выражено на языке Си. Мы хотим умножить два 32-битных числа и получить 64-битный результат, из которого мы возвращаем средний 32-битный. Однако в C это умножение не существует. Все, что вы можете сделать, это повысить целые числа до 64 бит и сделать умножение 64 * 64 = 64.
Однако x86 (и ARM, MIPS и другие) могут выполнять умножение в одной инструкции. Некоторые компиляторы игнорировали этот факт и генерировали код, который вызывает функцию библиотеки времени выполнения для выполнения умножения. Сдвиг на 16 также часто выполняется библиотечной подпрограммой (такой же сдвиг может выполнять и x86).
Таким образом, у нас остается один или два библиотечных вызова только для умножения. Это имеет серьезные последствия. Сдвиг не только медленнее, регистры должны сохраняться в вызовах функций, а также не помогают вставка и развертывание кода.
Если вы переписываете тот же код на (встроенном) ассемблере, вы можете значительно увеличить скорость.
В дополнение к этому: использование ASM - не лучший способ решения проблемы. Большинство компиляторов позволяют вам использовать некоторые инструкции на ассемблере во внутренней форме, если вы не можете выразить их в C. Например, компилятор VS.NET2008 выставляет 32 * 32 = 64-битное значение mul как __emul, а 64-битное смещение как __ll_rshift.
Используя встроенные функции, вы можете переписать функцию таким образом, чтобы у C-компилятора была возможность понять, что происходит. Это позволяет встроить код, распределить регистр, исключить общее подвыражение и постоянное распространение. Вы получите огромныйТаким образом, улучшение производительности по сравнению с рукописным ассемблерным кодом.
Для справки: конечный результат для mul с фиксированной точкой для компилятора VS.NET:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
Разница в производительности делителей с фиксированной точкой еще больше. У меня были улучшения до 10 раз для тяжелого кода с фиксированной запятой, написав пару asm-строк.
Использование Visual C ++ 2013 дает одинаковый код сборки для обоих способов.
gcc4.1 из 2007 также хорошо оптимизирует версию на чистом C. (В проводнике компилятора Godbolt не было установлено более ранних версий gcc, но, вероятно, даже более старые версии GCC могли бы делать это без встроенных функций.)
См. Source + asm для x86 (32-бит) и ARM в проводнике компилятора Godbolt . (К сожалению, у него нет достаточно старых компиляторов для создания плохого кода из простой версии на чистом C).
Современные процессоры могут делать вещи C не имеют операторов для вообще , как popcnt
и битые-сканирование , чтобы найти первый или последний набор бит . (POSIX имеет ffs()
функцию, но ее семантика не соответствует x86 bsf
/ bsr
. См. Https://en.wikipedia.org/wiki/Find_first_set ).
Некоторые компиляторы могут иногда распознавать цикл, который подсчитывает количество установленных битов в целом числе и компилировать его в popcnt
инструкцию (если она включена во время компиляции), но гораздо надежнее использовать __builtin_popcnt
в GNU C или в x86, если вы только нацеливание оборудования с SSE4.2: _mm_popcnt_u32
от<immintrin.h>
.
Или в C ++, присвойте std::bitset<32>
и используйте .count()
. (Это тот случай, когда язык нашел способ портативного представления оптимизированной реализации popcount через стандартную библиотеку, таким образом, который всегда будет компилироваться во что-то правильное и может использовать все, что поддерживает цель.) См. Также https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .
Аналогично, ntohl
может компилироваться в bswap
(32-битный байт подкачки x86 для преобразования в порядковый номер) в некоторых реализациях C, которые его имеют.
Другая важная область для встроенных функций или рукописного асма - это ручная векторизация с инструкциями SIMD. Компиляторы не плохи с простыми циклами, как dst[i] += src[i] * 10.0;
, но часто плохо или вообще не векторизации, когда все становится сложнее. Например, вы вряд ли получите что-то вроде Как реализовать Atoi с помощью SIMD? автоматически генерируется компилятором из скалярного кода.