webBG - програмисти, машинно обучение, javascript, python, php, питам, говорим, публикации

Причина за свиване на честотната лента на паметта, когато 2KB данни са кеширани в L1-кеша

В проект за самообучение измервам честотната лента на паметта с помощта на следния код (тук перифразиран, целият код следва в края на въпроса):

unsigned int doit(const std::vector<unsigned int> &mem){
   const size_t BLOCK_SIZE=16;
   size_t n = mem.size();
   unsigned int result=0;
   for(size_t i=0;i<n;i+=BLOCK_SIZE){           
             result+=mem[i];
   }
   return result;
}

//... initialize mem, result and so on
int NITER = 200; 
//... measure time of
   for(int i=0;i<NITER;i++)
       resul+=doit(mem)

BLOCK_SIZE се избира по такъв начин, че цял 64-байтов кеш ред се извлича за едно цяло число-добавяне. Моята машина (Intel-Broadwell) се нуждае от около 0,35 наносекунди за добавяне на цяло число, така че кодът по-горе може да насити честотна лента до 182GB/s (тази стойност е само горна граница и вероятно е доста отклонена, важното е съотношение на честотните ленти за различни размери). Кодът е компилиран с g++ и -O3.

Променяйки размера на вектора, мога да наблюдавам очакваните честотни ленти за L1(*)-, L2-, L3-кешове и RAM-паметта:

въведете описание на изображението тук

Има обаче ефект, който наистина се боря да обясня: срив на измерената честотна лента на L1-кеша за размери около 2 kB, тук в малко по-висока резолюция:

въведете описание на изображението тук

Бих могъл да възпроизведа резултатите на всички машини, до които имам достъп (които имат процесори Intel-Broadwell и Intel-Haswell).

Моят въпрос: Каква е причината за срива на производителността за размер на паметта около 2 KB?

(*) Надявам се, че разбирам правилно, че за L1-кеша не се четат/прехвърлят 64 байта, а само 4 байта на добавяне (няма допълнителен по-бърз кеш, където редът на кеша трябва да бъде попълнен), така че начертаната честотна лента за L1 е само горната граница, а не самата пропускателна способност.

Редактиране: Когато размерът на стъпката във вътрешния for-цикъл е избран да бъде

  • 8 (вместо 16) колапсът се случва за 1KB
  • 4 (вместо 16) колапсът се случва за 0,5KB

когато вътрешният цикъл се състои от около 31-35 стъпки/четения. Това означава, че колапсът не се дължи на размера на паметта, а на броя на стъпките във вътрешния цикъл.

Може да се обясни с пропуски на клонове, както е показано в страхотния отговор на @user10605163.


Списък за възпроизвеждане на резултатите

bandwidth.cpp:

#include <vector>
#include <chrono>
#include <iostream>
#include <algorithm>


//returns minimal time needed for one execution in seconds:
template<typename Fun>
double timeit(Fun&& stmt, int repeat, int number)
{  
   std::vector<double> times;
   for(int i=0;i<repeat;i++){
       auto begin = std::chrono::high_resolution_clock::now();
       for(int i=0;i<number;i++){
          stmt();
       }
       auto end = std::chrono::high_resolution_clock::now();
       double time = std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count()/1e9/number;
       times.push_back(time);
   }
   return *std::min_element(times.begin(), times.end());
}


const int NITER=200;
const int NTRIES=5;
const size_t BLOCK_SIZE=16;


struct Worker{
   std::vector<unsigned int> &mem;
   size_t n;
   unsigned int result;
   void operator()(){
        for(size_t i=0;i<n;i+=BLOCK_SIZE){           
             result+=mem[i];
        }
   }

   Worker(std::vector<unsigned int> &mem_):
       mem(mem_), n(mem.size()), result(1)
   {}
};

double PREVENT_OPTIMIZATION=0.0;


double get_size_in_kB(int SIZE){
   return SIZE*sizeof(int)/(1024.0);
}

double get_speed_in_GB_per_sec(int SIZE){
   std::vector<unsigned int> vals(SIZE, 42);
   Worker worker(vals);
   double time=timeit(worker, NTRIES, NITER);
   PREVENT_OPTIMIZATION+=worker.result;
   return get_size_in_kB(SIZE)/(1024*1024)/time;
}


int main(){

   int size=BLOCK_SIZE*16;
   std::cout<<"size(kB),bandwidth(GB/s)\n";
   while(size<10e3){
       std::cout<<get_size_in_kB(size)<<","<<get_speed_in_GB_per_sec(size)<<"\n";
       size=(static_cast<int>(size+BLOCK_SIZE)/BLOCK_SIZE)*BLOCK_SIZE;
   }

   //ensure that nothing is optimized away:
   std::cerr<<"Sum: "<<PREVENT_OPTIMIZATION<<"\n";
}

create_report.py:

import sys
import pandas as pd
import matplotlib.pyplot as plt

input_file=sys.argv[1]
output_file=input_file[0:-3]+'png'
data=pd.read_csv(input_file)

labels=list(data)    
plt.plot(data[labels[0]], data[labels[1]], label="my laptop")
plt.xlabel(labels[0])
plt.ylabel(labels[1])   
plt.savefig(output_file)
plt.close()

Изграждане/пускане/създаване на отчет:

>>> g++ -O3 -std=c++11 bandwidth.cpp -o bandwidth
>>> ./bandwidth > report.txt
>>> python create_report.py report.txt
# image is in report.png

  • Каква е вашата операционна система? 13.12.2018
  • @jxh Linux, не съм сигурен, че зависи от операционната система, по-скоро от процесора на Intel 13.12.2018
  • Вижте тази статия за ефекта от подравняването на страницата. Изглежда, че ефектът, който наблюдавате, е свързан. 13.12.2018
  • Функцията void doit(...) в горната част на вашия въпрос не прави нищо с result. gcc и clang го оптимизират напълно. godbolt.org/z/6P4sqh. gcc оптимизира цялата функция само до ret, докато clang все още зацикля правилния брой пъти, но не чете паметта. Ако сте върнали резултата, той не може да бъде оптимизиран. (Ако приемем, че повикващият също е направил нещо с него, за да провали оптимизацията след вграждането.) godbolt.org/z/4vjC9v 13.12.2018
  • Когато получавате L1 хитове, не е наистина точно да претендирате за прехвърлени 64 байта, когато четете само 4 байта. Имате нужда от процесор с AVX512, за да прочетете 64 байта от L1d с един uop, а използването на 512-битови вектори намалява максималния турбо часовник на текущите процесори на Intel. (Също така, без разгръщане на цикъл с множество акумулатори, вие ще затрудните забавянето от 1 цикъл на add вместо пропускателната способност 2 зареждане на такт L1d.) 13.12.2018

Отговори:


1

Промених леко стойностите: NITER = 100000 и NTRIES=1, за да получа по-малко шумен резултат.

В момента нямам наличен Broadwell, но изпробвах вашия код на моя Coffee-Lake и получих спад на производителността, не при 2KB, а около 4,5KB. Освен това откривам непостоянно поведение на пропускателната способност малко над 2KB.

Синята линия в графиката съответства на вашето измерване (лява ос):

Червената линия тук е резултатът от perf stat -e branch-instructions,branch-misses, даващ частта от разклоненията, които не са правилно прогнозирани (в проценти, дясна ос). Както можете да видите, има ясна антикорелация между двете.

Разглеждайки по-подробния доклад perf, открих, че основно всички тези погрешни прогнози на разклонения се случват в най-вътрешния цикъл в Worker::operator(). Ако взетият/незаетият модел за клона на цикъла стане твърде дълъг, предсказващият клон няма да може да го проследи и така изходният клон на вътрешния цикъл ще бъде погрешно предвиден, което води до рязък спад в пропускателната способност. С по-нататъшно увеличаване на броя на итерациите въздействието на това единично грешно прогнозиране ще стане по-малко значимо, което ще доведе до бавно възстановяване на пропускателната способност.

За допълнителна информация относно хаотичното поведение преди падането вижте коментарите, направени от @PeterCordes по-долу.

Във всеки случай най-добрият начин да се избегнат погрешни прогнози за разклонения е да се избегнат разклоненията и затова ръчно развих цикъла в Worker::operator(), като например:

void operator()(){
    for(size_t i=0;i+3*BLOCK_SIZE<n;i+=BLOCK_SIZE*4){
         result+=mem[i];
         result+=mem[i+BLOCK_SIZE];
         result+=mem[i+2*BLOCK_SIZE];
         result+=mem[i+3*BLOCK_SIZE];
    }
}

Развиването на 2, 3, 4, 6 или 8 итерации дава резултатите по-долу. Имайте предвид, че не коригирах блоковете в края на вектора, които бяха игнорирани поради разгръщането. Следователно периодичните пикове в синята линия трябва да се игнорират, долната граница на основната линия на периодичния модел е действителната честотна лента.

въведете описание на изображението тук въведете описание на изображението тук < img src="https://i.stack.imgur.com/VnQlQ.png" alt="въведете описание на изображението тук"> въведете описание на изображението тук въведете описание на изображението тук

Както можете да видите, частта от погрешните прогнози на разклоненията всъщност не се е променила, но тъй като общият брой на разклоненията е намален с коефициента на разгънатите итерации, те вече няма да допринасят силно за производителността.

Има и допълнителна полза от това, че процесорът е по-свободен да прави изчисленията извън реда, ако цикълът се развие.

Ако се предполага, че това има практическо приложение, бих предложил да се опитате да дадете на горещия цикъл фиксиран брой итерации по време на компилиране или някаква гаранция за делимост, така че (може би с някои допълнителни съвети) компилаторът да може да вземе решение за оптималния брой итерации за разгръщане.

13.12.2018
  • За достатъчно нисък брой итерации, предсказването на разклонения правилно предсказва разклонението за излизане от цикъла на най-вътрешния цикъл. напр. модел 21 взети, 1 невзет, 21 взет, 1 невзет. След около 22 или 23 итерации на Skylake, клонът за излизане от цикъл ще прогнозира погрешно, така че имате едно погрешно прогнозиране (при изход от вътрешния цикъл) на итерация на външния цикъл. Колкото повече итерации на вътрешен цикъл имате, толкова по-малка е общата цена. И да, разгръщането ще помогне много, позволявайки на извънредния изпълнител да работи напред и да види погрешното прогнозиране и не изисква 4 uops на тактова пропускателна способност, за да бъде в крак. 13.12.2018
  • @PeterCordes Благодаря за информацията. Основно съм изненадан от привидно произволните пикове около 2,5 в случая с неразвит цикъл. Например за size = 35*BLOCK_SIZE изглежда има едно грешно предвиждане за вътрешния цикъл, докато няма нито едно за 34*BLOCK_SIZE или 36*BLOCK_SIZE. 13.12.2018
  • TAGE предикторите използват скорошната история на разклоненията (взети/невзети) като част от индекс в кеша за предсказване на разклонения. Това е вид странност, която трябва да очаквате, когато сте на прага на изчерпване на BTB пространство или псевдоним: един модел прогнозира много добре, подобен модел се натъква на псевдоним между важни клонове и прогнозира лошо. (Действителната индексна функция обикновено е хеш на историята на клона и адреса на клона.) напр. irisa.fr/caps/people/seznec/L-TAGE.pdf и comparch.net/2013/06/30 /why-tage-is-the-best 13.12.2018
  • @user10605163 наистина изглежда, че пропуските в клоновете са причината. Изглежда, че наистина не разбирам какво става тук, защото това е пълна изненада за мен, изобщо играе роля... 13.12.2018
  • @ead: вложените цикли, където най-вътрешният цикъл има малък брой итерации, са проблем. Създава модел, че всяко 25-то изпълнение на същия клон не е взето, а останалите са взети. Неправилното предвиждане спира тръбопровода и води до отхвърляне на много работа, която е била в процес на полет. (И с неразвит цикъл, изпълнението извън реда не помага толкова, колкото би могло без препятствие в предния край.) Последната редакция на този отговор добави хубаво обобщение на този ефект. Вижте ръководството за микроархитектура на процесора на Agner Fog (agner.org/optimize) и stackoverflow.com/tags/x86/info за повече. 13.12.2018

  • 2

    Може да не е свързано, но вашата Linux машина може да играе с честотата на процесора. Знам, че Ubuntu 18 има управител, който е балансиран между мощност и производителност. Също така искате да си поиграете с афинитета на процеса, за да сте сигурни, че няма да бъде мигриран към друго ядро, докато работи.

    13.12.2018
    Нови материали

    Записване на грешки — Как да записвате грешки във вашето приложение, за да ги отстраните по-късно
    Записването на грешки е важна част от „обработването на грешки“. Накратко, когато възникнат определени грешки в програмите, вие искате да знаете за това. Това е особено важно при грешки. Ти..

    Кратко въведение в теорията на графите
    Кратко въведение в теорията на графите Втора част: внедряване на python на пълни графики В моята предишна статия въведох три основни концепции за графите: върхове, ръбове и тегла. В тази..

    Днес ще пиша за машинно обучение. Смятам се за ентусиаст, а не за експерт, така че имайте това предвид. С това казано, искам...
    Днес ще пиша за машинно обучение. Смятам се за ентусиаст, а не за експерт, така че имайте това предвид. С това казано искам да обясня защо машинното обучение има значение. За да бъда съвсем..

    Какво е структурно типизиране и как Typescript го използва в своя полза?
    Всички знаят тези дни, че „Typescript е строго синтактично надмножество на JavaScript и добавя незадължително статично въвеждане към езика.“. Но какво всъщност означава? Защо миграцията от..

    3 начина за премахване на дубликати от масив в Javascript
    Вие сте уеб разработчик? Програмист ли си? Тогава ще сте запознати с JavaScript и различните му вградени функции, методи и т.н. за различни реализации, проблеми и цели. Един от тези широко..

    Архитектура и обучение на конволюционни невронни мрежи (7 точки):
    Тази публикация предоставя подробности за архитектурата на Конволюционната невронна мрежа (CNN), функциите и обучението на всеки слой, завършвайки с резюме на обучението на CNN...

    Създайте разширение за Chrome с помощта на Angular
    Този урок е базиран на манифеста на разширението на chrome версия 3 (MV3), а също и на Angular версия 2+ (2, 3 и...). Ако не сте използвали манифест версия 3, можете да следвате този урок ,..