Коли збірка швидша за C?


475

Однією з заявлених причин того, щоб знати асемблера, є те, що іноді його можна використовувати для написання коду, який буде більш ефективним, ніж написання цього коду мовою вищого рівня, зокрема С. Тим НЕ менше, я також чув , що сказав багато разів , що , хоча це не зовсім брехня, випадки , коли може асемблер фактично бути використані для створення більш продуктивний код є вкрай рідко і вимагають спеціальних знань і досвіду з збірки.

Це питання навіть не входить у той факт, що інструкції асемблера будуть специфічними для машини та не переносяться, або будь-який з інших аспектів асемблера. Звичайно, існує безліч вагомих причин для того, щоб знати збірку, окрім цього, але це має бути конкретним питанням, що вимагає прикладів та даних, а не розширеним дискурсом про асемблер проти мов вищого рівня.

Чи може хто-небудь надати конкретні приклади випадків, коли збірка буде швидшою, ніж добре написаний код C, використовуючи сучасний компілятор, і чи можете ви підтримати цю заяву профайливими доказами? Я впевнений, що такі випадки існують, але мені дуже хочеться точно знати, наскільки езотеричні ці випадки, оскільки, здається, це є певною суперечкою.


17
насправді це досить тривіально вдосконалити після складеного коду. Кожен, хто має ґрунтовні знання мови складання та C, може побачити це, вивчивши створений код. Будь-який простий - це перший обрив продуктивності, з якого ви випадаєте, коли у вас складені одноразові регістри у складеній версії. У середньому компілятор буде робити набагато краще, ніж людина, для великого проекту, але в пристойному проекті не складно знайти проблеми з виконанням у складеному коді.
old_timer

14
Насправді, коротка відповідь: Ассемблер завжди швидший або рівний швидкості С. Причина полягає в тому, що ви можете мати збірку без С, але без С без складання (у двійковій формі, яку ми в старому днів називають "машинним кодом"). Це означає, що довга відповідь: компілятори C досить добре оптимізують та "думають" про речі, про які зазвичай не думають, тому це дійсно залежить від ваших навичок, але зазвичай ви завжди можете обіграти компілятор C; це все ще лише програмне забезпечення, яке не може думати та отримувати ідеї. Ви також можете написати портативний асемблер, якщо ви використовуєте макроси і ви терплячі.

11
Я абсолютно не погоджуюся з тим, що відповіді на це питання потрібно "базуватись на думці" - вони можуть бути досить об'єктивними - це не щось на зразок спроби порівняти ефективність улюблених мов домашніх тварин, для яких кожен матиме сильні моменти та може відступити. Це питання розуміння того, наскільки далеко можуть дістати компілятори, і з якого пункту краще перейняти.
jsbueno

21
Раніше в своїй кар'єрі я писав багато асемблерів C та мейнфреймів у програмній компанії. Одним із моїх однолітків було те, що я б назвав "пуристом асемблера" (все повинно бути асемблером), тому я думаю, що я можу написати задану процедуру, яка бігала швидше на С, ніж те, що він міг написати в асемблері. Я виграв. Але на завершення, після того, як я виграв, я сказав йому, що хочу другу ставку - що я можу написати щось швидше в асемблері, ніж програма C, яка перемогла його на попередній ставці. Я теж виграв це, доводячи, що більшість з них зводиться до майстерності та вміння програміста більше, ніж будь-що інше.
Валерія Р

3
Якщо ваш мозок не має -O3прапора, вам, мабуть, краще залишити оптимізацію компілятору C :-)
paxdiablo

Відповіді:


272

Ось приклад із реального світу: фіксована точка множиться на старих компіляторах.

Вони не тільки зручні на пристроях без плаваючої точки, вони світяться, коли справа доходить до точності, оскільки вони дають 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? генерується автоматично компілятором зі скалярного коду.


6
Як щодо таких речей, як {x = c% d; y = c / d;}, чи достатньо розумні компілятори, щоб зробити це єдиним дівом чи idiv?
Jens Björnhager

4
Власне, хороший компілятор створив би оптимальний код з першої функції. Захоронення вихідного коду за допомогою внутрішніх текстів або вбудованої вбудованої програми абсолютно без користі - це не найкраще.
ковзаючий

65
Привіт Слайкер, я думаю, що вам ніколи не доводилося працювати над критичним часом часовим кодом ... вбудована вбудована система може зробити * велику різницю. Крім того, для компілятора внутрішнє значення - це те саме, що і звичайна арифметика в C. В цьому і полягає суть внутрішніх значень. Вони дозволяють вам використовувати функцію архітектури, не стикаючись з недоліками.
Nils Pipenbrinck

6
@slacker Насправді код тут досить читабельний: вбудований код робить одну унікальну операцію, яку відразу зрозуміло зчитування підпису методу. Код втрачається лише повільно в читанні, коли використовується незрозуміла інструкція. Тут важливо, що у нас є метод, який виконує лише одну чітко визначену операцію, і це справді найкращий спосіб отримати читабельний код цих атомних функцій. До речі, це не настільки незрозумілий невеликий коментар, як / * (a * b) >> 16 * / не може відразу пояснити це.
Дерексон

5
Справедливо кажучи, цей приклад є поганим, принаймні сьогодні. Компілятори C давно можуть зробити множення 32x32 -> 64, навіть якщо мова не пропонує його безпосередньо: вони визнають, що коли ви передаєте 32-бітні аргументи на 64-бітні, а потім множите їх, не потрібно зробіть повне 64-розрядне множення, але 32x32 -> 64 буде добре. Я перевірив, і всі clang, gcc та MSVC у їхній поточній версії отримують це право . Це не нове - я пам'ятаю, дивлячись на вихід компілятора і помічаючи це десятиліття тому.
BeeOnRope

143

Багато років тому я вчив когось програмувати на C. Вправа полягала в тому, щоб повернути графіку на 90 градусів. Він повернувся з рішенням, яке потребувало декількох хвилин для завершення, головним чином тому, що він використовував множення та ділення тощо.

Я показав йому, як переробити проблему, використовуючи бітові зрушення, і час на обробку зменшився приблизно на 30 секунд на неоптимізувальному компіляторі, який він мав.

Щойно я отримав оптимізуючий компілятор і той самий код повернув графіку за <5 секунд. Я подивився на код складання, який створює компілятор, і з того, що я побачив, вирішив там і потім, що мої дні написання асемблера закінчилися.


3
Так, це була однобітна монохромна система, зокрема це були монохромні блоки зображень на Atari ST.
lilburne

16
Чи оптимізуючий компілятор склав оригінальну програму чи вашу версію?
Thorbjørn Ravn Andersen

На якому процесорі? На 8086 році я очікував, що оптимальний код для повороту 8x8 буде завантажувати DI з 16 бітами даних за допомогою SI, повторювати add di,di / adc al,al / add di,di / adc ah,ahі т. Д. Для всіх восьми 8-бітних регістрів, потім робити всі 8 регістрів ще раз, а потім повторити всю процедуру три більше разів, і нарешті збережіть чотири слова в ax / bx / cx / dx. Ні в якому разі асемблер не збирається наблизитися до цього.
supercat

1
Я дійсно не можу придумати будь-яку платформу, де компілятор, швидше за все, потрапить в коефіцієнт або два оптимального коду для обертання 8x8.
supercat

65

Практично будь-коли, коли компілятор бачить код з плаваючою комою, рукописна версія буде швидшою, якщо ви використовуєте старий поганий компілятор. ( Оновлення 2019 року: Це взагалі не стосується сучасних компіляторів. Особливо при компілюванні для чогось іншого, крім x87; компілятори мають простіший час із SSE2 або AVX для скалярної математики, або будь-який не-x86 з плоским набором реєстру FP, на відміну від x87 зареєструвати стек.)

Основна причина полягає в тому, що компілятор не може виконати жодних надійних оптимізацій. Дивіться цю статтю від MSDN для обговорення цього питання. Ось приклад, коли збіркова версія вдвічі перевищує швидкість, ніж версія C (складена з VS2K5):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

І деякі номери з мого ПК, на якому працює збірка версії за замовчуванням * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

Я не зацікавившись, я змінив цикл dec / jnz, і це не мало значення для синхронізації - іноді швидше, іноді повільніше. Я думаю, що пам'ять з обмеженим аспектом гномить інші оптимізації. (Примітка редактора: швидше за все, вузьке місце затримки FP достатньо, щоб приховати зайві витрати loop. Виконання двох підсумків Кахана паралельно для непарних / парних елементів та додавання їх в кінці, можливо, може прискорити це в 2 рази. )

Упс, я працював дещо іншою версією коду, і він виводив цифри неправильним способом (тобто C було швидше!). Виправлено та оновлено результати.


20
Або в GCC ви можете розв’язати руки компілятора щодо оптимізації з плаваючою комою (до тих пір, поки ви не обіцяєте нічого робити з нескінченностями або NaNs), використовуючи прапор -ffast-math. Вони мають рівень оптимізації, -Ofastякий на даний момент еквівалентний -O3 -ffast-math, але в майбутньому може включати в себе більше оптимізацій, які можуть призвести до неправильного генерування коду у кутових випадках (наприклад, код, що спирається на NaN-коди IEEE).
Девід Стоун

2
Так, поплавці не є комутативними, компілятор повинен робити ТОЧНО те, що ви написали, в основному те, що сказав @DavidStone.
Алек Тіл

2
Ви пробували математику SSE? Продуктивність була однією з причин, коли MS повністю відмовилася від x87 повністю у x86_64 та 80-бітовій подвійній у x86
phuclv

4
@Praxeolitic: FP add - комутативний ( a+b == b+a), але не асоціативний (переупорядкування операцій, тому округлення проміжних продуктів відрізняється). re: цей код: я не вважаю, що коментовані х87 та loopінструкція - це надзвичайно прихильна демонстрація швидкої ASM. loopмабуть, це насправді не вузьке місце через затримку ПП. Я не впевнений, чи він проводить операції з ПП, чи ні; x87 людям важко читати. Два fstp resultsінни в кінці явно не є оптимальними. Виведення додаткового результату зі стека краще зробити з магазином. Як і fstp st(0)IIRC.
Пітер Кордес

2
@PeterCordes: Цікавим наслідком створення коммутативного додавання є те, що хоча 0 + x і x + 0 еквівалентні один одному, жоден з них не завжди еквівалентний x.
Supercat

58

Не наводячи жодних конкретних прикладів чи доказів профілера, ви можете написати кращий асемблер, ніж компілятор, коли ви знаєте більше, ніж компілятор.

У загальному випадку сучасний компілятор C знає набагато більше про те, як оптимізувати розглянутий код: він знає, як працює конвеєр процесора, він може спробувати переупорядкувати інструкції швидше, ніж може людина, і так далі - це в основному те саме, що комп'ютер настільки ж хороший чи кращий, ніж найкращий гравець для настільних ігор тощо. Просто тому, що він може робити пошук у проблемному просторі швидше, ніж більшість людей. Хоча теоретично ви можете працювати так само добре, як і комп'ютер у певному випадку, ви, звичайно, не можете робити це з однаковою швидкістю, що робить його нездійсненним для більш ніж кількох випадків (тобто компілятор, безумовно, перевершить вас, якщо ви спробуєте написати більше декількох процедур в асемблері).

З іншого боку, є випадки, коли компілятор не має стільки інформації - я б сказав, перш за все, при роботі з різними формами зовнішнього обладнання, про які компілятор не має знань. Основним прикладом, ймовірно, є драйвери пристроїв, де асемблер у поєднанні з людськими інтимними знаннями про обладнання, про яке йде мова, може дати кращі результати, ніж це може зробити компілятор C

Інші згадали інструкції зі спеціальним призначенням, про що я говорю в параграфі вище - інструкції, щодо яких компілятор може мати обмежені знання або взагалі не мати знань, що дозволяє людині писати швидший код.


Взагалі це твердження вірно. Компілятор робить найкраще DWIW, але в деяких крайніх випадках асемблер кодування вручну виконує завдання, коли продуктивність у режимі реального часу необхідна.
подружжя

1
@Liedman: "вона може спробувати перевпорядкувати інструкції швидше, ніж може людина". OCaml відомий тим, що він швидкий, і, що дивно, його ocamloptнабір компілятора нативного коду пропускає планування інструкцій на x86 і, замість цього, залишає його до процесора, оскільки він може ефективніше переупорядкувати під час виконання.
Джон Харроп

1
Сучасні компілятори роблять багато, і це може зайняти занадто багато часу вручну, але вони ніде не досконалі. Шукайте помилки gcc або llvm про помилки "пропущена оптимізація". Тут багато. Крім того, коли ви пишете в ASM, ви можете легше скористатися передумовами типу "цей вхід не може бути негативним", що складно довести компілятору.
Пітер Кордес

48

У моїй роботі є три причини, щоб я знав і користувався монтажем. У порядку важливості:

  1. Налагодження - я часто отримую код бібліотеки, що містить помилки або неповну документацію. Я зрозумію, що це робить, вступивши на рівні складання. Я маю це робити приблизно раз на тиждень. Я також використовую його як інструмент для налагодження проблем, у яких мої очі не помічають ідіоматичної помилки в C / C ++ / C #. Дивлячись на збірку, минає це.

  2. Оптимізація - компілятор працює досить добре в оптимізації, але я граю в іншому бальному парку, ніж у більшості. Я пишу код обробки зображень, який зазвичай починається з коду, який виглядає приблизно так:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    типово "роби щось" зазвичай відбувається в кілька мільйонів разів (тобто від 3 до 30). Виконуючи цикли на етапі "роби щось", збільшення продуктивності значно збільшується. Я зазвичай не починаю з цього місця - я зазвичай починаю з написання коду, щоб спочатку працювати, а потім докладаю всіх зусиль, щоб переробляти C, щоб бути природно кращим (кращий алгоритм, менше навантаження в циклі тощо). Зазвичай мені потрібно прочитати збірку, щоб побачити, що відбувається, і рідко потрібно це писати. Я роблю це, можливо, кожні два-три місяці.

  3. щось робити мова мені не дозволить. Сюди можна віднести: отримання архітектури процесора та конкретних функцій процесора, доступ до прапорів, які не знаходяться в процесорі (людина, я дуже хотів би, щоб C надав вам доступ до прапора несучої) тощо. Це я роблю, можливо, раз на рік або два роки.


Ви не кахлюєте свої петлі? :-)
Джон Харроп

1
@plinth: як ви маєте на увазі "цикли вискоблювання"?
lang2

@ lang2: це означає позбутися якомога більше зайвого часу, проведеного у внутрішньому циклі - все, що компілятору не вдалося витягнути, що може включати використання алгебри для зняття множення з однієї петлі, щоб зробити її додаванням у внутрішній тощо
плінтус

1
Начеплення петлі здається непотрібним, якщо ви робите лише один пропуск даних.
Джеймс М. Лежав

@ JamesM.Lay: Якщо ви торкаєтесь кожного елемента лише один раз, кращий порядок переходу може надати вам просторову локальність. (Наприклад , використовувати всі байти рядки кешу , що ви стикнулися, замість зациклення вниз стовпців матриці , використовуючи один елемент в кожному рядку кеша.)
Пітер Кордес

42

Тільки при використанні якихось інструкцій спеціального призначення компілятор не підтримує.

Для максимізації обчислювальної потужності сучасного процесора за допомогою декількох конвеєрів та передбачуваного розгалуження потрібно структурувати програму складання таким чином, що людина робить а) майже неможливим написати б) ще більше неможливо підтримувати.

Крім того, кращі алгоритми, структури даних та управління пам'яттю дадуть вам принаймні на порядок більшу продуктивність, ніж мікрооптимізація, яку ви можете виконати при складанні.


4
+1, хоча останнє речення насправді не належить до цієї дискусії - можна припустити, що асемблер вступає в гру лише після того, як будуть реалізовані всі можливі вдосконалення алгоритму тощо.
mghie

18
@Matt: ASM, написаний від руки, часто набагато краще на деяких крихітних центральних процесорах, які EE працює з ними, які мають шалену підтримку компілятора постачальника.
Zan Lynx

5
"Тільки при використанні якихось наборів інструкцій спеціального призначення" ?? Ви, мабуть, ніколи раніше не писали фрагменту оптимізованого рукою коду ASM. Помірно інтимне знання архітектури, над якою ви працюєте, дає вам хороший шанс створити кращий код (розмір та швидкість), ніж ваш компілятор. Очевидно, що, як прокоментував @mghie, ви завжди починаєте кодувати найкращі альгоси, які ви можете створити для вас. Навіть для дуже хороших компіляторів вам дійсно потрібно писати свій код C таким чином, що приводить компілятор до найкращого зібраного коду. В іншому випадку згенерований код буде недооптимальним.
ysap

2
@ysap - на фактичних комп’ютерах (не крихітних вбудованих мікросхемах) в реальному світі використання "оптимальний" код не буде швидшим, оскільки для будь-якого великого набору даних продуктивність буде обмежена доступом до пам'яті та помилками сторінки ( і якщо у вас немає великого набору даних, це буде швидким і в будь-якому випадку, і немає сенсу його оптимізувати) - в ті дні я працюю здебільшого на C # (навіть не в), і продуктивність роботи від компакт-диспетчера пам’яті виходить, вагові витрати на збирання сміття, ущільнення та збирання JIT.
Нір

4
+1 за твердження, що компілятори (особливо JIT) можуть зробити кращу роботу, ніж люди, якщо вони оптимізовані для обладнання, на якому вони працюють.
Себастьян

38

Хоча C "близький" до низькорівневого маніпулювання 8-бітовими, 16-бітовими, 32-бітовими, 64-бітовими даними, є кілька математичних операцій, не підтримуваних C, які часто можуть бути виконані елегантно в певній інструкції зі складання набори:

  1. Множення з фіксованою точкою: Добуток двох 16-бітних чисел - це 32-бітове число. Але правила в С говорять, що добуток двох 16-бітних чисел - це 16-розрядне число, а добуток двох 32-розрядних чисел - 32-розрядне число - нижня половина в обох випадках. Якщо ви хочете, щоб верхня половина множини 16x16 або множина 32x32, вам потрібно грати в ігри з компілятором. Загальний метод полягає в тому, щоб передати на біт більшій від необхідної ширини, помножити, зрушити вниз і повернути назад:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    У цьому випадку компілятор може бути достатньо розумним, щоб знати, що ви насправді просто намагаєтеся розмножувати верхню половину 16x16 і робити все правильно з нативної версією машини 16x16. Або це може бути дурним і вимагати виклику бібліотеки, щоб перемножити 32x32, це є надмірним набором, тому що вам потрібно лише 16 біт продукту - але стандарт C не дає вам ніякого способу самовираження.

  2. Окремі операції по переміщенню в біт (обертання / перенесення):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    Це не дуже елегантно в C, але знову ж таки, якщо компілятор не є достатньо розумним, щоб зрозуміти, що ви робите, він буде робити багато "непотрібних" робіт. Багато наборів інструкцій для складання дозволяють повертати або зміщувати вліво / вправо результат з реєстром перенесення, тому ви могли виконати вищезазначене в 34 інструкціях: завантажте вказівник на початок масиву, очистіть перенесення та виконайте 32 8- біт-правий зсув, використовуючи автоматичне збільшення на вказівник.

    Для іншого прикладу, є лінійні регістри зсуву зворотного зв’язку (LFSR), які елегантно виконуються в збірці: візьміть шматок N біт (8, 16, 32, 64, 128 і т.д.), змістіть все це правильно на 1 (див. Вище алгоритм), то якщо результуюче перенесення дорівнює 1, то ви XOR у бітовій схемі, яка представляє поліном.

Сказавши це, я б не вдався до цих методів, якщо б не мав серйозних обмежень у виконанні. Як говорили інші, збірку набагато складніше документувати / налагоджувати / випробовувати / підтримувати, ніж код C: збільшення продуктивності пов'язане з серйозними витратами.

редагувати: 3. Виявлення переповнення можливе в зборі (насправді це не можна зробити на C), це полегшує деякі алгоритми.


23

Коротка відповідь? Іноді.

Технічно кожна абстракція має вартість, а мова програмування - це абстракція того, як працює процесор. C однак дуже близько. Роки тому я пам'ятаю, як сміявся вголос, коли я ввійшов у свій обліковий запис UNIX і отримав таке повідомлення про стан (коли такі речі були популярні):

Мова програмування на C - Мова, яка поєднує гнучкість мови асемблера та потужність мови монтажу.

Це смішно, бо це правда: C - це як портативна мова складання.

Варто зазначити, що мова збірки просто працює, однак ви її пишете. Однак є компілятор між C та мовою складання, яку він створює, і це надзвичайно важливо, оскільки те, наскільки швидкий ваш код C, має дуже багато спільного з тим, наскільки хороший ваш компілятор.

Коли gcc вийшов на сцену, однією з речей, які зробили його настільки популярним, було те, що він часто був набагато кращим, ніж компілятори C, що постачалися з багатьма комерційними ароматами UNIX. Мало того, що ANSI C (жоден із цього сміття K&R C) не був більш надійним і, як правило, створював кращий (швидший) код. Не завжди, але часто.

Я вам все це кажу, тому що немає правила про швидкість C і асемблера, оскільки немає об'єктивного стандарту для C.

Так само асемблер сильно відрізняється залежно від того, який процесор ви працюєте, вашої системної специфікації, набору інструкцій, який ви використовуєте тощо. Історично існували дві сім'ї архітектури процесора: CISC та RISC. Найбільшим гравцем у CISC була та залишається архітектура Intel x86 (та набір інструкцій). RISC домінував у світі UNIX (MIPS6000, Alpha, Sparc тощо). CISC виграв битву за серця та розум.

У будь-якому разі, популярна мудрість, коли я був молодшим розробником, полягав у тому, що написаний від руки x86 часто може бути набагато швидшим, ніж C, оскільки спосіб роботи архітектури мав складність, яка виграла від того, хто це робив. З іншого боку, RISC здавався розробленим для компіляторів, тому ніхто (я не знав) написав, що скаже, асемблер Sparc. Я впевнений, що такі люди існували, але, без сумніву, вони обоє звели з розуму і були інституціоналізовані.

Набори інструкцій є важливим моментом навіть в одній родині процесорів. Деякі процесори Intel мають розширення, такі як SSE через SSE4. AMD мала свої власні інструкції SIMD. Перевага такої мови програмування, як C, - це те, що хтось міг написати свою бібліотеку, тому вона була оптимізована для того, на якому процесорі ви працювали. Це була важка робота в асемблері.

Ще є оптимізації, які ви можете зробити в асемблері, які не може зробити жоден компілятор, і добре написана algoirthm асемблера буде настільки ж швидкою або швидшою, ніж це C-еквівалент. Питання більше: чи варто цього?

Зрештою, хоч ассемблер був продуктом свого часу і був більш популярним в той час, коли цикли процесора були дорогими. Сьогодні процесор, який коштує 5-10 доларів США для виготовлення (Intel Atom), може зробити майже все, що хто-небудь міг захотіти. Єдиною реальною причиною написання асемблера в ці дні є такі речі на низькому рівні, як деякі частини операційної системи (навіть тому переважна більшість ядер Linux написана на С), драйвери пристроїв, можливо, вбудовані пристрої (хоча С там, як правило, домінує теж) тощо. Або просто для ударів (що дещо мазохістично).


Було багато людей, які використовували асемблер ARM як мову вибору на машинах Acorn (початок 90-х). У IIRC вони сказали, що невеликий набір інструкцій з ризику полегшує та веселіше. Але я підозрюю, що це тому, що компілятор C запізнився на Acorn, а компілятор C ++ так і не був закінчений.
Ендрю М

3
"... тому що немає суб'єктивного стандарту для C." Ви маєте на увазі об'єктивну .
Томас

@AndrewM: Так, я писав змішані програми в BASIC і ARM ассемблері близько 10 років. Я навчився С за той час, але це було не дуже корисно, оскільки воно настільки громіздке, як асемблер і повільніше. Norcroft зробив кілька дивовижних оптимізацій, але, думаю, умовний набір інструкцій був проблемою для компіляторів цього дня.
Джон Харроп

1
@AndrewM: ну, насправді ARM - це такий собі РИСК, який робиться у зворотному напрямку. Інші МСБ RISC розроблені, починаючи з використання компілятора. ARM ISA, здається, був розроблений, починаючи з того, що надає процесор (перемикач бочки, прапори стану → давайте викриємо їх у кожній інструкції).
ninjalj

16

Випадок використання, який може не застосовуватися більше, але для вашого задоволення: На Amiga, процесор і графічні / аудіо-мікросхеми боротимуться за доступ до певної області ОЗУ (перші 2 Мб оперативної пам’яті мають бути конкретними). Тож, коли ви мали лише 2 Мб оперативної пам’яті (або менше), відображення складної графіки плюс відтворення звуку вбивало б продуктивність процесора.

У асемблері ви можете переплутати свій код таким розумним чином, що процесор намагатиметься отримати доступ до оперативної пам’яті лише тоді, коли графічні / аудіо чіпи були зайняті всередині (тобто коли шина була вільна). Таким чином, переупорядковуючи ваші вказівки, розумне використання кешу процесора, тактики шини, ви могли досягти деяких ефектів, які були просто неможливі, використовуючи мову вищого рівня, тому що вам доводилося виконувати кожну команду, навіть вставляти туди-сюди NOP, щоб зберегти різні мікросхеми радіолокатора.

Що є ще однією причиною того, чому інструкція процесора NOP (No Operation - не робити нічого) може фактично змусити вашу програму працювати швидше.

[EDIT] Звичайно, методика залежить від конкретної установки обладнання. Яка була основна причина, чому багато ігор Amiga не могли впоратися з більш швидкими процесорами: терміни вказівки вимкнулися.


У Amiga не було 16 Мбайт оперативної пам'яті, як 512 кБ до 2 Мб залежно від чіпсету. Крім того, багато ігор Amiga не працювали зі швидшими процесорами завдяки таким методам, як ви описуєте.
bk1e

1
@ bk1e - Amiga виробила великий асортимент різних моделей комп’ютерів, Amiga 500 поставляється з 512К таран, розширений до 1Meg в моєму випадку. amigahistory.co.uk/amiedevsys.html - це аміга зі 128Meg Ram
David Waters

@ bk1e: я виправлений. Моя пам’ять може мене вийти з ладу, але чип оперативної пам’яті не обмежений першим 24-бітовим адресним простором (тобто 16 Мб)? І Швидкий був відображений вище цього?
Аарон Дігулла

@Aaron Digulla: У Вікіпедії є додаткова інформація про відмінності між чіпом / швидкою / повільною ОЗУ: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e: Моя помилка. Процесор 68k мав лише 24 адресних смуги, тому я мав 16MB в голові.
Аарон Дігулла

15

Точка, яка не є відповіддю.
Навіть якщо ви ніколи не програмуєте в ньому, я вважаю корисним знати хоча б один набір інструкцій асемблера. Це частина нескінченного прагнення програмістів знати більше, а тому бути кращим. Також корисно, коли ви вступаєте в рамки, ви не маєте вихідного коду і маєте хоча б приблизне уявлення про те, що відбувається. Це також допомагає зрозуміти JavaByteCode та .Net IL, оскільки вони обидва схожі на асемблер.

Щоб відповісти на питання, коли у вас є невелика кількість коду або велика кількість часу. Найбільш корисно для використання у вбудованих мікросхемах, де низька складність чіпа та погана конкуренція у компіляторах, націлених на ці чіпи, можуть налагодити баланс на користь людей. Крім того, для обмежених пристроїв ви часто торгуєте розміром коду / розміром / продуктивністю пам'яті таким чином, що складно буде доручити компілятору. Наприклад, я знаю, що ця дія користувача не викликається часто, тому я матиму невеликий розмір коду та низьку продуктивність, але ця інша функція, схожа на використання, використовується щосекунди, тому у мене буде більший розмір коду та швидша продуктивність. Це такий вид торгівлі, який може використовувати кваліфікований програміст.

Я також хотів би додати, що є багато середини, де ви можете кодувати в C компіляції і вивчити створену збірку, а потім змінити код C або налаштувати і підтримувати як збірку.

Мій друг працює над мікроконтролерами, на даний момент мікросхемами для управління невеликими електродвигунами. Він працює в поєднанні низького рівня c і збірки. Одного разу він розповів мені про хороший день на роботі, коли він скоротив основний цикл з 48 інструкцій до 43. Він також стикається з такими варіантами, як код виріс, щоб заповнити 256k чіп, і бізнес хоче нової функції?

  1. Видаліть наявну функцію
  2. Зменшіть розмір деяких або всіх існуючих функцій, можливо, ціною продуктивності.
  3. Захистіть перехід на більшу мікросхему з більшою вартістю, більшим споживанням енергії та більшим форм-фактором.

Мені хотілося б додати як комерційного розробника з досить портфоліо або мовами, платформами, типами додатків, які я жодного разу не відчував необхідності зануритися в складання письмових зборів. Я як завжди оцінював знання, отримані з цього приводу. І іноді налагоджується в ньому.

Я знаю, що я набагато більше відповів на питання "чому я повинен навчитися асемблеру", але я вважаю, що це важливіше питання, коли це швидше.

тож давайте спробуємо ще раз Вам варто задуматися про збірку

  • працює над функцією операційної системи низького рівня
  • Робота над компілятором.
  • Робота над надзвичайно обмеженим чіпом, вбудованою системою тощо

Не забудьте порівняти свою збірку з створеним компілятором, щоб побачити, що швидше / менше / краще.

Девід.


4
+1 для розгляду вбудованих додатків на крихітних мікросхемах. Занадто багато інженерів програмного забезпечення або не вважають вбудованими, або вважають, що це означає розумний телефон (32 біт, оперативна пам’ять MB, флеш-пам'ять MB).
Мартін

1
Вбудовані в часі програми - прекрасний приклад! Часто є дивні інструкції (навіть дуже прості, такі як avr's sbiі cbi), якими компілятори, які раніше (а іноді й досі), не користуються повною мірою, через їх обмежене знання обладнання.
felixphew

15

Я здивований, що ніхто цього не сказав. strlen()Функція набагато швидше , якщо написано в зборі! У С найкраще, що ти можеш зробити

int c;
for(c = 0; str[c] != '\0'; c++) {}

при складанні ви можете значно пришвидшити це:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

довжина в ексе. Це порівнює 4 символи за один раз, тому це в 4 рази швидше. І подумайте, використовуючи слово високого порядку eax та ebx, воно стане в 8 разів швидшим , ніж попередній звичайний C!


3
Як це порівнюється з тими у strchr.nfshost.com/optimized_strlen_function ?
ninjalj

@ninjalj: вони те ж саме :) Я не думав, що це можна зробити так у C. Це можна трохи покращити, я думаю
BlackBear

Перед кожним порівнянням у C-коді ще є операція побітового І. Можливо, що компілятор буде досить розумним, щоб зменшити це до порівняння високого та низького байтів, але я не став би на це грошей. Насправді існує більш швидкий алгоритм циклу, який базується на властивості, яка (word & 0xFEFEFEFF) & (~word + 0x80808080)дорівнює нулю, якщо всі байти в слові є ненульовими.
користувач2310967

@MichaWiedenmann правда, я повинен завантажити bx після порівняння двох символів у ax. Дякую
BlackBear

14

Матричні операції з використанням інструкцій SIMD, ймовірно, швидше, ніж код, створений компілятором.


Деякі компілятори (VectorC, якщо я правильно пам'ятаю) генерують код SIMD, тому навіть це, мабуть, більше не є аргументом для використання асемблерного коду.
OregonGhost

Компілятори створюють код, що знає SSE, тому цей аргумент не відповідає дійсності
vartec

5
Для багатьох із цих ситуацій ви можете використовувати інтриси SSE замість складання. Це зробить ваш код більш портативним (gcc visual c ++, 64bit, 32bit і т.д.), і вам не доведеться робити розподіл реєстру.
Laserallan

1
Звичайно, ви хочете, але в цьому питанні не було питання, де я повинен використовувати збірку замість C. Він сказав, коли компілятор C не генерує кращий код. Я припустив джерело C, яке не використовує прямі SSE-дзвінки або вбудовану збірку.
Мехрдад Афшарі

9
Мехрдад, правда, правда. Отримати право SSE досить складно для компілятора, і навіть у очевидних (для людей ситуаціях) ситуаціях більшість компіляторів не використовують його.
Конрад Рудольф

13

Я не можу навести конкретні приклади, тому що це було занадто багато років тому, але було чимало випадків, коли власноруч написаний асемблер міг перевиконати будь-який компілятор. Причини, чому:

  • Ви можете відхилитися від виклику конвенцій, передаючи аргументи в регістри.

  • Ви можете уважно розглянути, як використовувати регістри та уникати збереження змінних у пам'яті.

  • У таких речах, як таблиці стрибків, ви можете уникнути необхідності перевіряти індекс.

В основному, компілятори роблять досить хорошу роботу з оптимізації, і це майже завжди "досить добре", але в деяких ситуаціях (наприклад, візуалізація графіки), де ви платите дорого за кожен цикл, ви можете приймати ярлики, оскільки ви знаєте код , де компілятор не міг, тому що він повинен бути в безпечній стороні.

Насправді я чув про код графічного візуалізації, в якому рутина, як звичайна лінія малювання рядків або полігон, фактично генерувала невеликий блок машинного коду на стеку і виконувала його там, щоб уникнути постійного прийняття рішень. про стиль лінії, ширину, візерунок тощо.

Однак, я хочу зробити компілятор - це створити для мене хороший код складання, але не бути надто розумним, і вони в основному роблять це. Насправді одна з речей, яку я ненавиджу щодо Фортран, - це його розшифровка коду в спробі "оптимізувати" його, як правило, без значної мети.

Зазвичай, коли програми мають проблеми з продуктивністю, це пов'язано з марнотратним дизайном. У ці дні я ніколи не рекомендував би асемблер для продуктивності, якщо загальний додаток уже не був налаштований протягом дюйма свого життя, все ще не був досить швидким, і весь свій час проводив у тісних внутрішніх петлях.

Додано: Я бачив безліч додатків, написаних мовою складання, і головна перевага швидкості порівняно з такою мовою, як C, Pascal, Fortran і т.д., тому що програміст був набагато обережнішим при кодуванні в асемблері. Він або вона збирається писати приблизно 100 рядків коду в день, незалежно від мови, і мовою компілятора, що дорівнює 3 або 400 інструкціям.


8
+1: "Ви можете відхилитися від умовних вимог". Компілятори C / C ++ прагнуть повернути кілька значень. Вони часто використовують щасливу форму, коли стек виклику виділяє безперервний блок для структури і передає посилання на нього для того, щоб його заповнити. Повернення кількох значень в регістри відбувається в кілька разів швидше.
Джон Харроп

1
@Jon: Компілятори C / C ++ роблять це добре, коли функція стає вбудованою (неінліновані функції повинні відповідати ABI, це не обмеження C і C ++, а пов'язуюча модель)
Ben Voigt,

@BenVoigt: Ось зустрічний приклад flyingfrogblog.blogspot.co.uk/2012/04/…
Джон Харроп

2
Я не бачу, щоб жоден виклик функції не вводився туди.
Бен Войгт

13

Кілька прикладів з мого досвіду:

  • Доступ до інструкцій, недоступних від C. Наприклад, багато архітектури (наприклад, x86-64, IA-64, DEC Alpha та 64-розрядні MIPS або PowerPC) підтримують 64-бітове 64-бітове множення, створюючи 128-бітний результат. Нещодавно GCC додав розширення, що забезпечує доступ до таких інструкцій, але перед цим було потрібне складання. І доступ до цієї інструкції може призвести до величезних змін у 64-бітних процесорах при впровадженні чогось типу RSA - іноді навіть у 4 рази покращення продуктивності.

  • Доступ до специфічних для процесора прапорів. Той, що мене багато покусав, - це прапор; виконуючи додавання з декількома точністю, якщо ви не маєте доступу до біта CPU для перенесення, потрібно замість цього порівняти результат, щоб побачити, чи він переповнюється, для чого потрібно ще 3-5 інструкцій на кінцівку; і ще гірше, які є досить послідовними з точки зору доступу до даних, що вбиває продуктивність на сучасних суперскалярних процесорах. При обробці тисяч таких цілих чисел підряд, можливість використання addc - це величезна виграш (також є суперскалярні проблеми з суперечкою на носі біта, але сучасні процесори досить добре справляються з цим).

  • SIMD. Навіть автовекторизація компіляторів може робити лише порівняно прості випадки, тому, якщо ви хочете гарної продуктивності SIMD, на жаль, часто доводиться писати код безпосередньо. Звичайно, ви можете використовувати внутрішні символи замість складання, але, як тільки ви знаходитесь на рівні внутрішньої роботи, ви в основному пишете збірку все одно, просто використовуючи компілятор як розподільник реєстру та (номінально) планувальник інструкцій. (Я, як правило, використовую внутрішню техніку для SIMD просто тому, що компілятор може генерувати функції прологів і те, що не для мене, тому я можу використовувати той самий код у Linux, OS X та Windows, не маючи справу з проблемами ABI, такими як функції виклику функцій, але інші ніж те, що справжні SSE справді не дуже приємні - Altivec здається кращим, хоча я не маю багато досвіду з ними).bitslicing AES або виправлення помилок SIMD - можна було б уявити компілятор, який би міг аналізувати алгоритми та генерувати такий код, але мені здається, що такий розумний компілятор знаходиться щонайменше за 30 років від існуючого (у кращому випадку).

З іншого боку, багатоядерні машини та розподілені системи змістили багато найбільших виграшів продуктивності в інший бік - отримайте додаткові 20% прискорення запису внутрішніх циклів у зборі, або 300%, провівши їх через декілька ядер, або 10000% на запускаючи їх по групі машин. І звичайно, оптимізацію високого рівня (такі речі, як ф'ючерси, запам'ятовування тощо) часто набагато простіше зробити в мові вищого рівня, наприклад, ML або Scala, ніж C або Asm, і часто можуть забезпечити набагато більший виграш. Отже, як завжди, слід робити компроміси.


2
@ Денніс, тому я написав: "Звичайно, ви можете використовувати внутрішню техніку замість складання, але як тільки ви перебуваєте на рівні внутрішніх даних, ви в основному пишете збірку все одно, просто використовуючи компілятор як розподільник реєстру та (номінально) планувальник інструкцій."
Джек Ллойд

Крім того, внутрішній код SIMD має тенденцію бути менш читабельним, ніж той самий код, записаний у асемблері. Багато SIMD-кодів покладається на неявні переінтерпретації даних у векторах, що є типовим для PITA, пов'язане з типом даних компілятора даних.
cmaster - відновити моніку

10

Об'ємні петлі, як при грі з зображеннями, оскільки зображення може містити мільйони пікселів. Сидячи і роздумуючи, як найкраще використовувати обмежену кількість регістрів процесорів, можна змінити значення. Ось зразок реального життя:

http://danbystrom.se/2008/12/22/optimizing-away-ii/

Тоді часто процесори мають деякі езотеричні інструкції, які занадто спеціалізовані для компілятора, щоб заважати, але іноді програміст асемблера може їх добре використати. Візьмемо, наприклад, інструкцію XLAT. Дійсно чудово, якщо вам потрібно зробити перегляд таблиці в циклі, і таблиця обмежена 256 байтами!

Оновлено: О, просто подумайте про те, що найважливіше, коли ми говоримо про петлі взагалі: компілятор часто не має поняття про те, скільки ітерацій буде загальним випадком! Тільки програміст знає, що цикл буде багато разів повторюватися і тому буде корисно підготуватися до циклу з деякою додатковою роботою, або якщо він буде повторений настільки кілька разів, що налаштування насправді займе більше часу, ніж ітерації очікуваний.


3
Оптимізація, спрямована на профіль, дає компілятору інформацію про те, як часто використовується цикл.
Зан Лінкс

10

Частіше, ніж ви думаєте, C потрібно робити речі, які здаються непотрібними з точки зору кодера Асамблеї лише тому, що так говорять стандарти C.

Промоція цілої кількості, наприклад. Якщо ви хочете змінити змінну char на C, зазвичай слід очікувати, що код дійсно зробить саме це, єдиний бітовий зсув.

Однак стандарти примушують компілятор робити знак, розширений на int перед зсувом, і обрізати результат після того, як позначити знак, що може ускладнити код залежно від архітектури цільового процесора.


Якісні компілятори для невеликих мікроспромінів роками не могли обробляти верхні частини значень у випадках, коли це ніколи не може суттєво вплинути на результати. Правила просування викликають проблеми, але найчастіше у тих випадках, коли компілятор не може знати, які кутові випадки є, а не актуальними.
Supercat

9

Ви насправді не знаєте, чи добре написаний код C справді швидкий, якщо ви не дивились на розбирання того, що виробляє компілятор. Багато разів ви дивитесь на це і бачите, що «добре написане» було суб’єктивним.

Тому не потрібно писати в асемблер, щоб отримати найшвидший код коли-небудь, але, безумовно, варто знати асемблер саме з тієї ж причини.


2
"Тому не потрібно писати в асемблер, щоб отримати найшвидший код коли-небудь" Ну, я не бачив, щоб компілятор робив оптимальну справу в будь-якому випадку, що не було тривіальним. Досвідчена людина може зробити краще, ніж компілятор практично у всіх випадках. Отже, абсолютно потрібно написати в асемблері, щоб отримати "найшвидший код за всю історію".
cmaster - відновити моніку

@cmaster На мій досвід вихід компілятора добре, випадковий. Іноді це дійсно добре і оптимально, а іноді - "як це сміття могло бути викинуто".
shartooth

9

Я прочитав усі відповіді (більше 30) і не знайшов просту причину: асемблер швидший за С, якщо ви читали та практикували посібник з оптимізації архітектури Intel® 64 та IA-32 , тому причина монтажу може бути повільніше, що люди, які пишуть такі повільніші збори, не читали Посібник з оптимізації .

У старі добрі часи Intel 80286 кожна інструкція виконувалася з фіксованою кількістю циклів процесора, але, оскільки Pentium Pro, випущений у 1995 році, процесори Intel стали надзвичайно масштабними, використовуючи складний конвеєр: Виконання поза замовлення та перейменування реєстру. До цього на Pentium, випущеному в 1993 році, існували U і V трубопроводи: подвійні трубопроводи, які могли виконати дві прості інструкції за один тактовий цикл, якщо вони не залежали одна від одної; але це не було чим порівнювати те, що перейменування поза замовленнями та перейменування реєстрів з’явилося в Pentium Pro, і майже не змінилося в наші дні.

Щоб пояснити кількома словами, найшвидший код - це те, коли інструкції не залежать від попередніх результатів, наприклад, ви завжди повинні очищати цілі регістри (за movzx) або використовувати add rax, 1натомість або inc raxдля усунення залежності від попереднього стану прапорів тощо.

Ви можете прочитати докладніше про Виконання поза замовленнями та Перейменування реєстру, якщо час дозволяє, в Інтернеті є багато інформації.

Існують також інші важливі питання, такі як прогнозування філій, кількість одиниць завантаження та зберігання, кількість воріт, які виконують мікро-операції тощо, але найголовніше, що слід враховувати, це саме виконання поза замовленням.

Більшість людей просто не знають про виконання поза замовленнями, тому вони пишуть свої програми складання, як-от 80286, очікуючи, що їх інструкція займе певний час для виконання незалежно від контексту; в той час як компілятори C знають про виконання поза замовлення і правильно генерують код. Ось чому код таких необізнаних людей повільніше, але якщо вам стане відомо, ваш код стане швидшим.


8

Я думаю, що загальний випадок, коли асемблер проходить швидше, це коли розумний програміст збірки дивиться на висновок компілятора і каже "це важливий шлях до продуктивності, і я можу написати це для більш ефективного", а потім ця людина налаштовує цей асемблер або переписує його з нуля.


7

Все залежить від вашої завантаженості.

Для щоденних операцій C і C ++ просто чудові, але є певні робочі навантаження (будь-які перетворення, що включають відео (стиснення, декомпресія, ефекти зображення тощо)), які в значній мірі вимагають складання для виконання.

Вони зазвичай передбачають використання специфічних для процесора розширень чіпсету (MME / MMX / SSE / будь-які інші), які налаштовані на такі операції.


6

У мене є операція транспозиції бітів, яку потрібно зробити, на 192 або 256 біт за кожне переривання, що відбувається кожні 50 мікросекунд.

Це відбувається за фіксованою картою (апаратні обмеження). Використовуючи C, на виготовлення знадобилося близько 10 мікросекунд. Коли я переклав це на Assembler, враховуючи специфічні особливості цієї карти, специфічне кешування реєстру та використовуючи бітові орієнтовані операції; для виконання знадобилося менше 3,5 мікросекунд.


6

Можливо, варто поглянути на Оптимізація непорушних та чистоти Вальтера Брайта, це не тестування з профілем, але показує вам один хороший приклад різниці між рукописним та створеним компілятором ASM. Уолтер Брайт пише оптимізацію компіляторів, так що, можливо, варто переглянути його інші публікації в блозі.



5

Проста відповідь ... Той , хто знає , що збірку добре (він же має посилання поруч з ним, і, користуючись кожної невеликої кеш - пам'яті процесора і трубопровідної функції і т.д.) гарантовано буде здатний виробляти набагато швидше , ніж код будь-якого компілятора.

Однак різниця в ці дні просто не має значення в типовому застосуванні.


1
Ви забули сказати, що "давали багато часу та сил" та "створювали кошмар на технічне обслуговування". Мій колега працював над оптимізацією критичного продуктивного розділу коду ОС, і він працював у C набагато більше, ніж збирання, оскільки це дозволило йому дослідити вплив ефективності змін на високому рівні в розумні часові рамки.
Артелій

Я згоден. Іноді ви використовуєте макроси та скрипти для створення коду асемблери, щоб заощадити час та швидко розвиватися. Більшість асемблерів сьогодні мають макроси; якщо ні, ви можете зробити (простий) передпроцесор макросу, використовуючи (досить простий RegEx) сценарій Perl.

Це. Точно. Компілятор для перемоги над експертами домену ще не винайдений.
cmaster - відновити моніку

4

Однією з можливостей версії PolyPascal CP / M-86 (побратим на Turbo Pascal) була заміна засобу "використання біоса для виведення символів на екран" машинною рутинною мовою, яка по суті було дано x, і y, і рядок, яку слід поставити туди.

Це дозволило оновити екран набагато, набагато швидше, ніж раніше!

У двійковій кімнаті було місце для вбудовування машинного коду (кілька сотень байтів), і там були й інші речі, тому важливо було максимально стиснути.

Виявляється, що оскільки екран був 80х25, обидві координати могли вміщуватися в байт кожен, тому обидві могли вміщуватися у двобайтовому слові. Це дозволило зробити обчислення, необхідні в меншій кількості байтів, оскільки одна доба може одночасно управляти обома значеннями.

Наскільки мені відомо, не існує компіляторів C, які могли б об'єднати кілька значень у регістрі, виконайте інструкції SIMD щодо них та розділіть їх знову пізніше (і я не думаю, що інструкції на машині все одно будуть коротшими).


4

Один з найбільш відомих фрагментів складання - це петля текстурування Майкла Абраша ( детально розкрита тут ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

На сьогоднішній день більшість компіляторів виражають розширені конкретні CPU інструкції як внутрішні елементи, тобто функції, які складаються до фактичної інструкції. MS Visual C ++ підтримує внутрішні компоненти для MMX, SSE, SSE2, SSE3 та SSE4, тому вам доведеться менше турбуватися про перехід на збірку, щоб скористатися певними інструкціями платформи. Visual C ++ також може скористатися фактичною архітектурою, на яку ви орієнтуєтесь, за допомогою відповідного налаштування / ARCH.


Ще краще, що ці SSE властивості визначені Intel, тому вони насправді досить портативні.
Джеймс

4

Враховуючи правильного програміста, програми Assembler завжди можна зробити швидше, ніж їхні C-колеги (принаймні, незначно). Було б складно створити програму C, де ви не змогли взяти хоча б одну інструкцію Асемблера.


Це було б трохи правильніше: "Було б важко створити нетривіальну програму C, де ..." Як варіант, можна сказати: "Було б важко знайти програму C в реальному світі, де ..." , є тривіальні петлі, для яких компілятори дають оптимальний вихід. Тим не менш, хороша відповідь.
cmaster - відновити моніку


4

gcc став широко використовуваним компілятором. Його оптимізація в цілому не така вже й добра. Набагато краще, ніж середній програміст, що пише програміст, але для реальної продуктивності - не так добре. Є компілятори, які просто неймовірні в коді, який вони виробляють. Отже, як загальна відповідь, буде багато місць, де ви можете зайти у висновок компілятора і налаштувати асемблер для продуктивності та / або просто переписати процедуру з нуля.


8
GCC робить надзвичайно розумні оптимізації, незалежні від платформи. Однак, не так добре використовувати конкретні набори інструкцій в повній мірі. Для такого портативного компілятора це дуже добре справляється.
Артелій

2
домовились. Його портативність, мови, що надходять, та цілі, що виходять, є дивовижними Будучи таким портативним, можна і дійсно заважає бути справді хорошим в одній мові або цілі. Тож можливості для людини зробити краще для конкретної оптимізації конкретної цілі.
old_timer

+1: GCC, безумовно, не є конкурентоспроможним у створенні швидкого коду, але я не впевнений, що це тому, що він портативний. LLVM портативний, і я бачив, як він генерує код в 4 рази швидше, ніж GCC.
Джон Харроп

Я вважаю за краще GCC, оскільки він був рок-солідом протягом багатьох років, плюс він доступний майже для кожної платформи, яка може запускати сучасний портативний компілятор. На жаль, мені не вдалося створити LLVM (Mac OS X / PPC), тому я, ймовірно, не зможу перейти на нього. Одне з хороших речей щодо GCC полягає в тому, що якщо ви пишете код, який будується в GCC, ви, швидше за все, дотримуєтесь стандартів, і ви будете впевнені, що його можна побудувати майже для будь-якої платформи.

4

Longpoke, є лише одне обмеження: час. Коли у вас немає ресурсів, щоб оптимізувати кожну зміну коду і витратити свій час на розподіл регістрів, оптимізувати кілька проливів, а що ні, компілятор виграє кожен раз. Ви вносите модифікацію коду, перекомпілюєте та вимірюєте. Повторіть, якщо потрібно.

Крім того, ви можете багато зробити на стороні високого рівня. Крім того, перевірка отриманої збірки може дати ВІДПОВІДЬ, що код є лайно, але на практиці він запуститься швидше, ніж те, що, на вашу думку, було би швидше. Приклад:

int y = дані [i]; // зробіть тут деякі речі .. call_function (y, ...);

Компілятор прочитає дані, підштовхне їх до стека (розливу) та пізніше прочитає зі стека та передасть як аргумент. Звучить сором? Насправді це може бути дуже ефективною компенсацією затримки і привести до швидшого виконання.

// оптимізована версія call_function (дані [i], ...); // не настільки оптимізований зрештою ..

Ідея з оптимізованою версією полягала в тому, що ми знизили тиск у регістрі та уникаємо розливу. Але по правді, "лайна" версія була швидшою!

Дивлячись на код складання, просто дивитись на інструкції та робити висновок: більше інструкцій, повільніше - це було б неправильним судженням.

На що тут слід звернути увагу: багато фахівців з монтажу думають, що вони знають багато, але знають дуже мало. Правила змінюються також від архітектури до наступної. Наприклад, немає срібного кулі x86, наприклад, який завжди найшвидший. У ці дні краще проходити за правилами:

  • пам'ять повільна
  • кеш-пам'ять швидка
  • спробуйте краще використовувати кешований
  • як часто ви збираєтесь сумувати? у вас є стратегія компенсації затримки?
  • ви можете виконати 10-100 інструкцій ALU / FPU / SSE для однієї пропуски кешу
  • архітектура додатків важлива ..
  • .. але це не допомагає, коли проблема не в архітектурі

Крім того, надто довіряти компілятору магічно перетворюючи погано продуманий код C / C ++ у "теоретично оптимальний" код є бажаним продумати. Ви повинні знати компілятор та ланцюжок інструментів, якими ви користуєтесь, якщо ви дбаєте про "продуктивність" на цьому низькому рівні.

Компілятори в C / C ++, як правило, не дуже добре переоформлюють під вирази, оскільки для початківців функції мають побічні ефекти. Функціональні мови не страждають від цього застереження, але так добре не відповідають сучасній екосистемі. Існують варіанти компілятора, які дозволяють розслаблені правила точності, які дозволяють змінювати порядок операцій компілятором / генератором / генератором коду.

Ця тема трохи тупикова; для більшості це не актуально, а решта вони все-таки знають, що роблять.

Все зводиться до цього: "зрозуміти, що ти робиш", це трохи відрізняється від того, щоб знати, що ти робиш.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.