Разходка през съвременния CPU pipeline

Rouge... Roge... Wrong... r... абе нещо-си-Conf 2014

Автор Борислав Станимиров

@natcbb

Какво е тази лекция?


  • Препускане през материята
  • Почти всяка точка от нея може да се разгледа детайлно в една или повече такива лекции
  • Много неща, само ще споменем и няма да разгледаме
  • Много неща няма и да споменем
  • Ще пропуснем много детайли
  • Ще се фокусираме върху х86

Някои основни понятия

Цикъл или тик (Cycle/Tick)

  • Основната единица за време за процесора. Честота
  • Отлично мерило за сложността на дадена операция
  • Процесорът казва: "цик... цик... цик..."

Инструкция

  • "Пътечка" сред транзисторите на процесора
  • Наборът от инструкции e Instruction set

Регистър

  • Парче данни в рамките на процесора
  • Работи супермегабързо

Други екстри


  • CU - цар, ALU, FPU, MMX, SSE ... - боклици
  • Конкретни регистри
  • Конкретни инструкции
  • Комуникация с външни (за процесора) устройства


Няма да се занимаваме с тях.

Ако някой се интересува, "But How Do It Know" е една чудесна книга по въпроса:

http://www.buthowdoitknow.com

Видове процесори

Има много начини да се класифирицират:

Например по instruction set:

  • CISC - x86
  • RISC - ARM, SPARC, PowerPC

Или по предназначение:

  • GPUs, CUDA - правят фреймбуфер, растеризират
  • Други акселератори

За тях може би ще си говорим на следващия OpenFest


Микроконтролери, виртуално CPU, екзотики, MISC, и др.

CISC и RISC


Ужасно много си приличат

С тези разлики:

  • Различни имена за практически еднакви неща
  • Дъъ... instruction set. RISC пести ток.
  • Load/Store и Register Memory
  • Други дреболии


Фокусираме се върху х86

Лето 1978: Intel 8086



  • Готини неща
  • Набор от готини регистри (AX-DX, адреси, IP)
  • Набор от готини инструкции
  • Готина съвместимост със цялата рода. Много неща са добавяни, но не са махани
  • PCDOS на готин Core i7

Една инструкция на 8086

  • Цик. Прочитаме какво има в IP
  • Цик. Подготвяме инструкцията - входа
  • Цик. Изпълняваме инструкцията
  • Цик. Пенсионираме инструкцията - изхода
  • Цик. Инкрементираме IP


... и всичко това се случва само в идеалния вариант.

Четене на данни. Wait state. Празни цикли.

Модерни иновации


  • 80286: Instruction cache
  • 80386: Data cache
  • 80386: Translation lookaside buffer cache (TLB)
  • L2, L3, L-амнайсе cache

Стига cache (благодарете ми с cache, в брой или на ръка)

  • 80486: Първи pipeline. Big Success. Средно двойно по-добра производителност от еквивалентен по честота 386

486 pipeline


  • Fetch - взимаме от instruction cache
  • Decode 1 - определяме пътечката за инструкцията
  • Decode 2 - приготвяме входа
  • Execution - изпълняваме инструкцията
  • Writeback - попълваме изхода


Всичко това в 5 нива:

                    1:  [F ][D1][D2][EX][WB]
                    2:      [F ][D1][D2][EX][WB]
                    3:          [F ][D1][D2][EX][WB]
                    4:              [F ][D1][D2][EX][WB]
                    5:                  [F ][D1][D2][EX][WB]
                    cpu: цик цик цик цик цик цик цик цик цик
                    

Проблеми с 486 pipeline

Има ги очевидните, наследени от преди: Преджуркване на cache → Wait state → Sad panda

Нов специфичен проблем: pipeline stall

Размяна на стойности

      XOR a, b; XOR b, a; XOR a, b;

Как работи това в нашия pipeline?


        XOR a,b: [F ][D1][D2][EX][WB]
        XOR b,a:     [F ][D1][PS][PS][D2][EX][WB]
        XOR a,b:         [F ][D1][PS][PS][PS][PS][D2][EX][WB]
        cpu:      цик цик цик цик цик цик цик цик цик цик цик
                    

Stall се получава и когато инструкция трябва да чака

Pentium Христос - спасителя

ООО че яко. Out-of-order execution. 486 на стероиди

Хитро взимаме до 32 инструкции от instruction cache

Взимаме до 4 инструкции от fetch buffer

Разталпваме ги на µ-ops - парчета от инструкции

Подреждаме входа в регистри по 4 µ-ops на цикъл

Копираме ги в тайни регистри за паралелизъм

Преподреждаме µ-ops в подходящ ред за изпълнение

ООО Меджик. Изпълняваме µ-ops по ултрамегабърз начин

Заемаме максимално количество портове (execution units)

Изчакваме всички µ-ops за инструкция

Попълваме изхода в подходящия ред

ООО че яко



Решихме pipeline stall проблема!

За програмата и програмиста цялата тая магия изглежда не по-различна от дедо 8086

ООО че не чак толкова яко

Създадохме нов проблем

... когато имаме branching

... инвестираме прекалено много

Получаваме Pipeline stall с чудовищни размери

  • Чакаме да приключат всички заредени инструкции
  • Правим roll-back до предишно състояние
  • Рестартираме целия pipeline на новото място

Когато имаме сто (или повече) инструкции, заредени в pipeline-a, това е неприемливо

Спекулативно изпълнение


Отговорът на нашия проблем

  • При branch изпълняваме всичките му разклонения
  • Когато разберем кое разклонение е правилното, изхвърляме резултатите от другото на боклука
  • Да, има разхищение, но многократно по-малко
  • Можем да поскажем на процесора кой branch да избере (__builtin_expect)

Разходка през съвременния CPU pipeline

За програмата светът изглежда така, както е изглеждал в 8086

Но за инструкцията...

Да вземем една инструкция, чиято програма върви

Животът е спокоен

В един момент хитрият prefetch я пъха в instruction cache

Може би на около 1500 инструкции от нея

Instruction pointer наближава

На около 24 инструкции от нея, тя бива пъхната заедно с 5 свои съседи във fetch buffer

Идва и реда да влезе в декодера

Той я разглобява на (примерно) 5 µ-ops

Декодера разбира че тя има нужда от допълнителни данни

На другия край на света, данните се напъхват в data cache

µ-op-овете и влизат в register alias table

За тях се разпределят нужните "фалшиви" регистри

µ-op-овете и влизат в reorder buffer

При първа възможност се пращат в reservation station

Някои от тях веднага се изпълняват. Тя не знае защо

Други от µ-op-овете и чакат, докато най-различни µ-op-ове от други инструкции се изпълняват

... и чакат

... и чакат още

Най сетне даните им са заредени

Те се изпълняват

Вече изцяло изпълнената инструкция се сглобява на излизане от reservation station и резултатите и са готови

На края, в последната фаза, тя се озовава в опашка със съседите си от самото начало, за да напусне процесора

Поуки


...които важат само за native компилираните програми

...и които може би знаехте преди (но сега знаете защо)

  • Ако програмата ни няма branching, ще работи по-бързо
  • Добре е данните са в гъсто пакетирани масиви (поредни в паметта)
  • Не можем току-така да кажем колко време ще отнеме изпълнението на дадена инструкция
  • По-простият код обикновено работи по-бързо

Още матрял



Край



Борислав Станимиров / ibob.github.io / @natcbb


Тази презентация е тук: http://ibob.github.io/slides/cpupbg/
Презентацията е лицензирана с Creative Commons Признание 3.0
Creative Commons License

Бонус

Как работи Hyper-Threading?