Вот пример из реальной жизни: умножение с фиксированной запятой на старых компиляторах.
Они не только пригодятся на устройствах без плавающей запятой, они сияют, когда дело доходит до точности, так как они дают вам 32 бита с предсказуемой ошибкой (у плавающего есть только 23 бита, и труднее предсказать потерю точности). то есть равномерная абсолютная точность во всем диапазоне вместо почти равномерной относительной точности (float
).
Современные компиляторы оптимизируют этот пример с фиксированной запятой, поэтому для более современных примеров, которые все еще нуждаются в коде, специфичном для компилятора, см.
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-компилятора была возможность понять, что происходит. Это позволяет встраивать код, распределять регистры, исключать общее подвыражение и постоянное распространение. Таким образом, вы получите огромное повышение производительности по сравнению с рукописным ассемблерным кодом.
Для справки: конечный результат для мульта с фиксированной запятой для компилятора 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? автоматически генерируется компилятором из скалярного кода.