Ось приклад із реального світу: фіксована точка множиться на старих компіляторах.
Вони не тільки зручні на пристроях без плаваючої точки, вони світяться, коли справа доходить до точності, оскільки вони дають 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-бітове. Однак у С цього множення не існує. Все, що ви можете зробити - це просунути цілі числа до 64 біт і зробити множення 64 * 64 = 64.
Однак x86 (і ARM, MIPS та інші) можуть виконувати множення в одній інструкції. Деякі компілятори використовували для ігнорування цього факту та генерування коду, який викликає функцію бібліотеки виконання, щоб зробити множення. Зсув на 16 також часто виконується бібліотечною програмою (також x86 може робити такі зрушення).
Таким чином, нам залишається один або два дзвінки з бібліотеки лише для множення. Це має серйозні наслідки. Мало того, що зміна відбувається повільніше, регістри повинні зберігатися через виклики функцій, і це не допомагає вбудовувати і розкручувати код.
Якщо ви перезаписуєте той самий код у (inline) асемблері, ви можете отримати значне збільшення швидкості.
На додаток до цього: використання ASM - не найкращий спосіб вирішити проблему. Більшість компіляторів дозволяють використовувати деякі інструкції асемблера у внутрішньому вигляді, якщо ви не можете їх виразити у C. Наприклад, компілятор VS.NET2008 демонструє 32 * 32 = 64 бітову муль як __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 могли це зробити без внутрішніх даних.)
Дивіться джерело + 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
(x86 32-бітний байт своп для конвертації ендіан) у деяких C-реалізаціях, у яких є.
Інша основна область для внутрішньої роботи або рукописного asm - це ручна векторизація з інструкціями SIMD. Компілятори непогані в таких простих петлях dst[i] += src[i] * 10.0;
, але часто роблять погано або взагалі не векторизуються, коли все ускладнюється. Наприклад, ви навряд чи отримаєте щось на кшталт Як реалізувати atoi за допомогою SIMD? генерується автоматично компілятором зі скалярного коду.