Низкоуровневые оптимизации Андрей Аксенов , Sphinx Highload++2011
Про что доклад
Про что доклад Высокие нагрузки = >  надо быстро Надо быстро  =>  надо оптимизировать Надо оптимизировать  =>  и так, и эдак Про высокий уровень говорить затрудняюсь С примерами плохо, без примеров плохо Поговорю про низкий уровень
Низкий уровень ? Это, конечно, ассемблер И только ассемблер Иначе быть не может Однако !
Низкий уровень
Низкий уровень Однако  “ ассемблеров ”  в 2011 году много C99, C++03, C++11, Java, C#, LLVM Поговорим, выходит, про  C/C++ Несмотря, что auto f = [](){}; // this is valid fucking C++ now
Почему  C/C++ Все еще ближе всего к железу Все еще топовые компиляторы Все еще НОД ,  интегрируется везде Все еще много стороннего софта на Ну и я   других  “ ассемблеров ”   “ не знаю ” “ Ну то есть что считать за секс ”
Про …  лозунги RT3D == Batch, batch, batch (c) ATI/Nvidia VLDB == Shard, shard, shard (c) everyone Multicore == Thread, thread, thread (c) Intel TDD == Test, test, test (c) K.Beck? Agile == Sprint, sprint, sprint (c) M.Fowler? Optimizations == ???
Нам лозунг строить…
… а вам с ним жить BENCH, BENCH, BENCH А давайте его отваром ромашки тогда. Толку не будет, но вреда тоже  (c)  Гиппократ
256 оттенков серого Но нас интересуют только 3 В целом –   диск ,  память , CPU Сегодня – память ,   CPU Завтра –  Ulrich Drepper, Agner Fog Звоните, все трое и звоните
Что нужно знать про  RAM Как соотносятся разные цены доступа Что память работает не байтами … Что бывает  L1/L2  кеш Что кеш вымывается Что  ( не)выравнивание небесплатно Что бывает (и иногда работает) префетч
Что нужно знать про  CPU Что есть регистры, инструкции ,  кеш кода Что есть целая арифметика,  FPU, MMX, SSE Что есть  mu-ops, pipes, branch prediction, register renaming, out-of-order execution Что все эти чудеса стоят разных  “ денег ” Что  amd64   “ лучше ” i686,  когда не  “ хуже ”
RAM,  про цены в целом Bandwidth.  Тупое  линейное  чтение 4.6 GB/sec  на лаптопе ,  12.7 GB/sec  на сервере Ништяк, ништяк? Latency.  Такие же  100M int32, stride 4096 195 MB/sec , 2.14 sec/100M,  ~49 Mreads/sec Те. последовательный доступ   ~1-2  такта Те. случайный доступ   ~40-60  тактов
RAM,  про цены на практике Читать один поток надо мало… но редко 100 M x { *c++ = (*a++) + (*b++); } Линейно   1111 MB/s  (вместо  4400+) С шагом  4KB  выходит  36 MB/s  ( вместо  195) Падение в 5 .4  раза Жалких  9 Mop/s,  240  тактов  на одну  (omfg)
RAM, L1  кеш Почему так ?  Потому что  L1/L2 cache Скачем с шагом  N =>   кеш-миссы  =>  тормоза Шаг 4..64,  ~ 4400 ..330 MB/sec, ~2x/ шаг Шаг 64 ..1024, ~ 330 ..195 MB/sec Значит, размер  L1  кеш-линии 64 байта  :)
RAM, L2  кеш А давайте иначе : вы мне – свою душу, а я вам – домашний адрес епископа Диомида?
RAM, L2  кеш Фиксируем шаг  1024 , уменьшаем  данные 100M, …, 4M, 3M, 2.3M == 195 MB/sec 2M == 648 MB/sec 1M == 1688 MB/sec 512K == 1724 MB/sec Все сходится, на лаптопе  2MB L2 cache
RAM,  про выравнивание Лаптоп , 4.6 vs 4.2 G/sec,  минус  8.3% Сервер , 12.7 vs 11.6 G/sec,  минус 7.3 % Intel, SPARC, ARM
RAM,  выводы
RAM,  таки выводы Если данных   много, лучше линейно Если линейно никак, лучше мало данных ! Если можно переложить Тетрис, то нужно Если можно, лучше всякое выровнять Но держать баланс с локальностью!
 
 
CPU,  про регистры i286 – ax, bx, cx, dx, si, di, bp, sp i386 – eax, ebx, ecx, edx, esi, edi, ebp, esp i686 – eax, ebx, ecx, edx, esi, edi, ebp, esp amd64 – rax..rsp, r0..r31 fpu (aka i287) – fp(0)..fp(7) sse – xmm0..xmm7
CPU,  про инструкции this->a += b mov eax, [esi+12] ; The Load add eax, ebx ; The Hit mov [esi+12], eax ; The Store a += b add eax, ebx ; The Hit
CPU, VS 2005 vs gcc 4.x this->a vs a member write-pressure int iRes = m_iCounter; // skip nonwhitespace while ( m_iCounter<m_iLimit &&  m_sBuffer[m_iCounter] ) m_iCounter++; test1 = 701 msec, test2 = 413 msec
CPU,  про кеш кода Код это тоже данные и тоже память… Внезапно, функции и конвенции вызова Внезапно, инлайнить или нет? Функции , inline, __forceinline , итп мычки Классы, реализация в декларации Шаблоны, функторы против коллбэков
CPU,  про креативное Pipes,  начиная с  i586 (Pentium-1) add eax, ebx add ecx, edx ; pairs Дальше ситуация ухудшилась!!! Mu-ops, renaming, out-of-order,  и всякое другое увлекательное вуду
CPU,  про ветвления if ( a!=0 ) { Doit(); } … cmp eax, 0 jne Label1 ; branch point call Doit Label1: … likely(), unlikely() etc
CPU,  еще   про ветвления switch() vs  простыня простых  if() Ни разу не эквивалентны! !! switch()  оптимизируется,  if()  нет Эффект заметен от 3 (трех) значений
CPU, if-for vs for-if for ( int i=0; i<NITER; i++ )  res += ( a==b ) ? c*d-e : (a+b)*c+d; For-if,  0.274 sec , res 1 300 000 000 If-for,  0.184 sec , res 1 300 000 000
CPU,  про  FPU, SSEx Дивный отдельный мир fpusum, 44042 usec, msvc default fpusum, 15386 usec, msvc /fp:fast ssesum, 8453 usec __m128 res = _mm_set_ps1(0.0f); while ( p<pmax ) res = _mm_add_ps(res, *p++);
CPU,  про  FPU, SSEx gcc 4.4.3, amd64 fpusum, 11214 usec ssesum, 3414 usec
CPU, SSE unroll ssesum, 8453 usec ssesum x4, 8240 usec ssesum x4 + non-naïve, 8056 usec, +5% res = _mm_add_ps(res, _mm_add_ps(_mm_add_ps(p[0], p[1]), _mm_add_ps(p[2], p[3]))); p += 4;
 
Что нужно знать про  cc/libc Компилятор ==  gcc, MSVC ,  ( припадочно ) ICC Про флаги сборки  (arch, etc) Про   конвенции вызова Про волшебные  define- ы
cc,  флаги сборки gcc, -march=(i386|i686|core2???) Умолчания системы могут удивить msvc, /arch: ( sse|sse2 ) , /fp:(fast| precise |… ) По умолчанию голимый  FPU msvc, /ZI (aka E&C, edit and continue) По умолчанию включен … “language”, my ass
__cdecl, __stdcall, __fastcall intrinsics (msvc) aka builtin functions (gcc) А это не магия !   fabs(), strcpy(),  итп _SECURE_SCL  итп отладочные итераторы msvc 2005  по умолчанию … __cdecl, NO intrinsics, _SECURE_SCL 1 cc,  конвенции вызова
cc,  аллокации Старые недобрые  malloc()/free() Drop-in  замены : nedmalloc, tcmalloc Мало  “ больших ”   (4-16K+)  везде ок Много мелких аллокаций везде боль Например, 1  M  аллокаций по 16 байт Ручные пулы все еще работают!
Боевой пример Морфологический словарь , libaot Сторонний код, между прочим! Как именно удалось его взгреть в  3  раза Что делает тот критичный код ? возвращает набор лемм по словоформе СТАЛИ  ->  СТАТЬ, СТАЛЬ
История любви ref, wall 11.9 magic1,  wall 10.1,  1.178x relative, 1.178x total magic123, wall 9.3, 1.086x   relative, 1.279x total currpath, wall 8.0, 1.162x relative, 1.487x total noformsort, wall 6.9, 1.159x relative, 1.724x total fastuc, wall 6.3, 1.095x relative, 1.888x total dwordres, wall 5.9, 1.067x relative, 2.016x total ptrres, wall 4.2, 1.404x relative, 2.833x total manrecurse, wall 4.0 , 1.050x relative, 2.975x total
Шаги 1, 2 +17.8%  (magic1) Выкидываем 1-буквенные  “ слова ” Выкидываем слова  “ нерусские ” +8.6%  (magic23) Выкидываем наиболее частые  2/3- буквенные И ведь всего - то 18 слов
Шаг 3 Было std::string currentPath; DoRecursiveStuff ( currentPath, … ); Стало   +16.2%  (currpath) BYTE sPath[128]; DoRecursiveStuff ( sPath, 0, … );
Шаг 4 Было vector< pair<string,int> > forms; Get ( forms ); Sort ( forms ); return forms[0];
Шаг 4 Стало   +15.9%  (noformsort) vector<int> indexes; string currentBest; Get ( indexes ); for ( int i=0; i<indexes.size(); i++ ) CheckAndUpdate ( currentBest ); return currentBest;
Шаг 5 Был фарш  RmlMakeUpper, FilterSrc Приводило регистр, заменяло букву Ё Стала прегенерация  BYTE m_UC[256]; while ( *p ) { *p = m_UC[*p]; p++; } +9.5%  (fastuc)
Шаг 6 Гонялся и возвращался  vector<WORD[3]>,  получаемый распаковкой некоего  DWORD Заменил на  vector<DWORD>,  в нужных местах добавил распаковку на месте +9.5%  (dwordres)
Шаг 7 /8 Результат писало в  vector<DWORD> & Infos Заменил на  DWORD[ 12 ],  с маркером конца Максимальная длина результата 6 +40.4%  (ptrres) !!! vector<DWORD> g_res + g_res.reserve()  толк тоже давали, но меньше (очевидно)
Шаг  8/8 Развернул  inner loop  из рекурсии в цикл Было int Count = GetChildrenCount(NodeNo); for … Стало   +5%  (manrecurse) int iChild[MAX_DEPTH], iChildMax[MAX_DEPTH]; while ( iLvl>=0 ) while ( iChild[iLvl]<iMax[iLvl] ) …
Неявный шаг номер 0 BENCH, BENCH, BENCH Как следствие  profile, profile, profile Черновик, замер, чистовик Десяток чистовиков был закоммитан Десяток черновиков сразу выкинут
Сводка фокусов Душим  fastpath , прямо в коде Шаги 1 , 2 Душим  RAM/stack pressure  аргументов Шаги  3, 4, 6, 7,  8 Душим сложность, тупое упрощение влоб!!! Шаг 5
Сводка других фокусов Душим лишнюю индирекцию Душим аллокации, пулим всякое Душим использование  RAM ( локальность!) Душим  “ плохие ”  общие структуры Душим  arch-specific  фокусы  (switch, sse, memb-pressure, for-if, ptr-walks, movzx, …)
Сводка третьих фокусов И еще несколько десятков всяких удивительных мелочей Из которых лично я…
Мораль про приемы Приемов много, общее правило одно Идеально вообще не работать !!!
 
Мораль про приемы Но уж если работать приходится, то по минимуму Как именно схалявить – вопрос не всегда простой Приходится проявляться изворотливость!
Мораль про процесс Запрофайлил, забенчмаркал, попробовал Намылил, прополоскал, отжал, повторил Каждый лишний 1% не лишний Курочка по зернышку …
ВОПРОСЫ ?!!

Низкоуровневая Оптимизация (Андрей Аксенов)

  • 1.
  • 2.
  • 3.
    Про что докладВысокие нагрузки = > надо быстро Надо быстро => надо оптимизировать Надо оптимизировать => и так, и эдак Про высокий уровень говорить затрудняюсь С примерами плохо, без примеров плохо Поговорю про низкий уровень
  • 4.
    Низкий уровень ?Это, конечно, ассемблер И только ассемблер Иначе быть не может Однако !
  • 5.
  • 6.
    Низкий уровень Однако “ ассемблеров ” в 2011 году много C99, C++03, C++11, Java, C#, LLVM Поговорим, выходит, про C/C++ Несмотря, что auto f = [](){}; // this is valid fucking C++ now
  • 7.
    Почему C/C++Все еще ближе всего к железу Все еще топовые компиляторы Все еще НОД , интегрируется везде Все еще много стороннего софта на Ну и я других “ ассемблеров ” “ не знаю ” “ Ну то есть что считать за секс ”
  • 8.
    Про … лозунги RT3D == Batch, batch, batch (c) ATI/Nvidia VLDB == Shard, shard, shard (c) everyone Multicore == Thread, thread, thread (c) Intel TDD == Test, test, test (c) K.Beck? Agile == Sprint, sprint, sprint (c) M.Fowler? Optimizations == ???
  • 9.
  • 10.
    … а вамс ним жить BENCH, BENCH, BENCH А давайте его отваром ромашки тогда. Толку не будет, но вреда тоже (c) Гиппократ
  • 11.
    256 оттенков серогоНо нас интересуют только 3 В целом – диск , память , CPU Сегодня – память , CPU Завтра – Ulrich Drepper, Agner Fog Звоните, все трое и звоните
  • 12.
    Что нужно знатьпро RAM Как соотносятся разные цены доступа Что память работает не байтами … Что бывает L1/L2 кеш Что кеш вымывается Что ( не)выравнивание небесплатно Что бывает (и иногда работает) префетч
  • 13.
    Что нужно знатьпро CPU Что есть регистры, инструкции , кеш кода Что есть целая арифметика, FPU, MMX, SSE Что есть mu-ops, pipes, branch prediction, register renaming, out-of-order execution Что все эти чудеса стоят разных “ денег ” Что amd64 “ лучше ” i686, когда не “ хуже ”
  • 14.
    RAM, процены в целом Bandwidth. Тупое линейное чтение 4.6 GB/sec на лаптопе , 12.7 GB/sec на сервере Ништяк, ништяк? Latency. Такие же 100M int32, stride 4096 195 MB/sec , 2.14 sec/100M, ~49 Mreads/sec Те. последовательный доступ ~1-2 такта Те. случайный доступ ~40-60 тактов
  • 15.
    RAM, процены на практике Читать один поток надо мало… но редко 100 M x { *c++ = (*a++) + (*b++); } Линейно 1111 MB/s (вместо 4400+) С шагом 4KB выходит 36 MB/s ( вместо 195) Падение в 5 .4 раза Жалких 9 Mop/s, 240 тактов на одну (omfg)
  • 16.
    RAM, L1 кеш Почему так ? Потому что L1/L2 cache Скачем с шагом N => кеш-миссы => тормоза Шаг 4..64, ~ 4400 ..330 MB/sec, ~2x/ шаг Шаг 64 ..1024, ~ 330 ..195 MB/sec Значит, размер L1 кеш-линии 64 байта :)
  • 17.
    RAM, L2 кеш А давайте иначе : вы мне – свою душу, а я вам – домашний адрес епископа Диомида?
  • 18.
    RAM, L2 кеш Фиксируем шаг 1024 , уменьшаем данные 100M, …, 4M, 3M, 2.3M == 195 MB/sec 2M == 648 MB/sec 1M == 1688 MB/sec 512K == 1724 MB/sec Все сходится, на лаптопе 2MB L2 cache
  • 19.
    RAM, провыравнивание Лаптоп , 4.6 vs 4.2 G/sec, минус 8.3% Сервер , 12.7 vs 11.6 G/sec, минус 7.3 % Intel, SPARC, ARM
  • 20.
  • 21.
    RAM, такивыводы Если данных много, лучше линейно Если линейно никак, лучше мало данных ! Если можно переложить Тетрис, то нужно Если можно, лучше всякое выровнять Но держать баланс с локальностью!
  • 22.
  • 23.
  • 24.
    CPU, прорегистры i286 – ax, bx, cx, dx, si, di, bp, sp i386 – eax, ebx, ecx, edx, esi, edi, ebp, esp i686 – eax, ebx, ecx, edx, esi, edi, ebp, esp amd64 – rax..rsp, r0..r31 fpu (aka i287) – fp(0)..fp(7) sse – xmm0..xmm7
  • 25.
    CPU, проинструкции this->a += b mov eax, [esi+12] ; The Load add eax, ebx ; The Hit mov [esi+12], eax ; The Store a += b add eax, ebx ; The Hit
  • 26.
    CPU, VS 2005vs gcc 4.x this->a vs a member write-pressure int iRes = m_iCounter; // skip nonwhitespace while ( m_iCounter<m_iLimit && m_sBuffer[m_iCounter] ) m_iCounter++; test1 = 701 msec, test2 = 413 msec
  • 27.
    CPU, прокеш кода Код это тоже данные и тоже память… Внезапно, функции и конвенции вызова Внезапно, инлайнить или нет? Функции , inline, __forceinline , итп мычки Классы, реализация в декларации Шаблоны, функторы против коллбэков
  • 28.
    CPU, прокреативное Pipes, начиная с i586 (Pentium-1) add eax, ebx add ecx, edx ; pairs Дальше ситуация ухудшилась!!! Mu-ops, renaming, out-of-order, и всякое другое увлекательное вуду
  • 29.
    CPU, проветвления if ( a!=0 ) { Doit(); } … cmp eax, 0 jne Label1 ; branch point call Doit Label1: … likely(), unlikely() etc
  • 30.
    CPU, еще про ветвления switch() vs простыня простых if() Ни разу не эквивалентны! !! switch() оптимизируется, if() нет Эффект заметен от 3 (трех) значений
  • 31.
    CPU, if-for vsfor-if for ( int i=0; i<NITER; i++ ) res += ( a==b ) ? c*d-e : (a+b)*c+d; For-if, 0.274 sec , res 1 300 000 000 If-for, 0.184 sec , res 1 300 000 000
  • 32.
    CPU, про FPU, SSEx Дивный отдельный мир fpusum, 44042 usec, msvc default fpusum, 15386 usec, msvc /fp:fast ssesum, 8453 usec __m128 res = _mm_set_ps1(0.0f); while ( p<pmax ) res = _mm_add_ps(res, *p++);
  • 33.
    CPU, про FPU, SSEx gcc 4.4.3, amd64 fpusum, 11214 usec ssesum, 3414 usec
  • 34.
    CPU, SSE unrollssesum, 8453 usec ssesum x4, 8240 usec ssesum x4 + non-naïve, 8056 usec, +5% res = _mm_add_ps(res, _mm_add_ps(_mm_add_ps(p[0], p[1]), _mm_add_ps(p[2], p[3]))); p += 4;
  • 35.
  • 36.
    Что нужно знатьпро cc/libc Компилятор == gcc, MSVC , ( припадочно ) ICC Про флаги сборки (arch, etc) Про конвенции вызова Про волшебные define- ы
  • 37.
    cc, флагисборки gcc, -march=(i386|i686|core2???) Умолчания системы могут удивить msvc, /arch: ( sse|sse2 ) , /fp:(fast| precise |… ) По умолчанию голимый FPU msvc, /ZI (aka E&C, edit and continue) По умолчанию включен … “language”, my ass
  • 38.
    __cdecl, __stdcall, __fastcallintrinsics (msvc) aka builtin functions (gcc) А это не магия ! fabs(), strcpy(), итп _SECURE_SCL итп отладочные итераторы msvc 2005 по умолчанию … __cdecl, NO intrinsics, _SECURE_SCL 1 cc, конвенции вызова
  • 39.
    cc, аллокацииСтарые недобрые malloc()/free() Drop-in замены : nedmalloc, tcmalloc Мало “ больших ” (4-16K+) везде ок Много мелких аллокаций везде боль Например, 1 M аллокаций по 16 байт Ручные пулы все еще работают!
  • 40.
    Боевой пример Морфологическийсловарь , libaot Сторонний код, между прочим! Как именно удалось его взгреть в 3 раза Что делает тот критичный код ? возвращает набор лемм по словоформе СТАЛИ -> СТАТЬ, СТАЛЬ
  • 41.
    История любви ref,wall 11.9 magic1, wall 10.1, 1.178x relative, 1.178x total magic123, wall 9.3, 1.086x relative, 1.279x total currpath, wall 8.0, 1.162x relative, 1.487x total noformsort, wall 6.9, 1.159x relative, 1.724x total fastuc, wall 6.3, 1.095x relative, 1.888x total dwordres, wall 5.9, 1.067x relative, 2.016x total ptrres, wall 4.2, 1.404x relative, 2.833x total manrecurse, wall 4.0 , 1.050x relative, 2.975x total
  • 42.
    Шаги 1, 2+17.8% (magic1) Выкидываем 1-буквенные “ слова ” Выкидываем слова “ нерусские ” +8.6% (magic23) Выкидываем наиболее частые 2/3- буквенные И ведь всего - то 18 слов
  • 43.
    Шаг 3 Былоstd::string currentPath; DoRecursiveStuff ( currentPath, … ); Стало +16.2% (currpath) BYTE sPath[128]; DoRecursiveStuff ( sPath, 0, … );
  • 44.
    Шаг 4 Былоvector< pair<string,int> > forms; Get ( forms ); Sort ( forms ); return forms[0];
  • 45.
    Шаг 4 Стало +15.9% (noformsort) vector<int> indexes; string currentBest; Get ( indexes ); for ( int i=0; i<indexes.size(); i++ ) CheckAndUpdate ( currentBest ); return currentBest;
  • 46.
    Шаг 5 Былфарш RmlMakeUpper, FilterSrc Приводило регистр, заменяло букву Ё Стала прегенерация BYTE m_UC[256]; while ( *p ) { *p = m_UC[*p]; p++; } +9.5% (fastuc)
  • 47.
    Шаг 6 Гонялсяи возвращался vector<WORD[3]>, получаемый распаковкой некоего DWORD Заменил на vector<DWORD>, в нужных местах добавил распаковку на месте +9.5% (dwordres)
  • 48.
    Шаг 7 /8Результат писало в vector<DWORD> & Infos Заменил на DWORD[ 12 ], с маркером конца Максимальная длина результата 6 +40.4% (ptrres) !!! vector<DWORD> g_res + g_res.reserve() толк тоже давали, но меньше (очевидно)
  • 49.
    Шаг 8/8Развернул inner loop из рекурсии в цикл Было int Count = GetChildrenCount(NodeNo); for … Стало +5% (manrecurse) int iChild[MAX_DEPTH], iChildMax[MAX_DEPTH]; while ( iLvl>=0 ) while ( iChild[iLvl]<iMax[iLvl] ) …
  • 50.
    Неявный шаг номер0 BENCH, BENCH, BENCH Как следствие profile, profile, profile Черновик, замер, чистовик Десяток чистовиков был закоммитан Десяток черновиков сразу выкинут
  • 51.
    Сводка фокусов Душим fastpath , прямо в коде Шаги 1 , 2 Душим RAM/stack pressure аргументов Шаги 3, 4, 6, 7, 8 Душим сложность, тупое упрощение влоб!!! Шаг 5
  • 52.
    Сводка других фокусовДушим лишнюю индирекцию Душим аллокации, пулим всякое Душим использование RAM ( локальность!) Душим “ плохие ” общие структуры Душим arch-specific фокусы (switch, sse, memb-pressure, for-if, ptr-walks, movzx, …)
  • 53.
    Сводка третьих фокусовИ еще несколько десятков всяких удивительных мелочей Из которых лично я…
  • 54.
    Мораль про приемыПриемов много, общее правило одно Идеально вообще не работать !!!
  • 55.
  • 56.
    Мораль про приемыНо уж если работать приходится, то по минимуму Как именно схалявить – вопрос не всегда простой Приходится проявляться изворотливость!
  • 57.
    Мораль про процессЗапрофайлил, забенчмаркал, попробовал Намылил, прополоскал, отжал, повторил Каждый лишний 1% не лишний Курочка по зернышку …
  • 58.