Как работает кеш процессора — и почему твой код ТОРМОЗИТ! | CPU Memory 1

Как работает кеш процессора? Почему кеш-френдли код ускоряет программу в 2 раза и больше? В этом видео я показываю на примерах, как CPU cache помогает сделать blazing fast код. Как просто переставив поле в структуре, можно ускорить программу в два раза!

*https://blog.itempuniversity.com/how-cpu-cache-worksand-why-your-code-slows-cpu-memory-1/
**https://www.youtube.com/watch?v=FwNGKhT06sM
***https://300.ya.ru/v_amv0F4eN

таймкоды

00:00:02 Введение в программу на Go

  • Определение константы 100 миллионов и структуры из трёх полей.
  • Создание слайса и измерение времени выполнения программы.
  • Сложение значений поля b структур и вывод на печать.

00:01:01 Результаты запуска программы

  • Первый запуск занимает 805 миллисекунд.
  • Последующие запуски ускоряются до 96–106 миллисекунд.
  • Перемещение поля a в структуре ускоряет код в два раза.

00:01:57 Стек и куча

  • Объяснение разницы в скорости работы стека и кучи.
  • Стек более компактен и не требует локатора.
  • Оба типа памяти находятся в оперативной памяти.

00:02:37 Скорость оперативной памяти

  • Оперативная память работает медленно по сравнению с процессором.
  • Процессор использует шину для взаимодействия с памятью.
  • Шина и память имеют одинаковую скорость.

00:04:28 Кэш процессора

  • Кэш первого уровня L1D и L1I ускоряет выполнение операций.
  • Если данных нет в L1D, проверяется L2.
  • При отсутствии данных в L2 процессор обращается к оперативной памяти.

00:06:28 Тайминги кэша и оперативной памяти

  • Чтение из кэша первого уровня занимает 3 такта.
  • Чтение из кэша второго уровня — 14 тактов.
  • Чтение из оперативной памяти — 240 тактов.

00:08:18 Типы памяти

  • Статическая память SRAM используется в кэше процессора.
  • Динамическая память DRAM применяется в оперативной памяти.
  • SRAM быстрее, но дороже. DRAM дешевле и меньше по размеру.

00:10:10 Преимущества DRAM

  • DRAM требует обновления ячеек каждые 64 миллисекунды.
  • Конденсаторы в DRAM имеют задержку в разряжении.
  • DRAM дешевле и занимает меньше места.

00:11:00 Заключение

  • Кэш процессора работает быстро благодаря SRAM.
  • Оперативная память медленная из-за DRAM.
  • Различия в скорости объясняются физическими ограничениями DRAM.

00:11:38 Введение в кэш

  • Оперативная память медленная, кэш значительно ускоряет доступ к данным.
  • Кэш хранит данные в виде кэш-линий, которые загружаются целиком при необходимости.
  • При отсутствии данных в кэше происходит кэш-мисс, и данные загружаются из оперативной памяти.

00:12:38 Размер кэш-линии и кэш-хиты

  • Размер кэш-линии может быть 64 или 128 байт.
  • Если данные в массиве или слайсе расположены плотно, процессор загружает всю кэш-линию.
  • Кэш-хит означает быстрый доступ к данным — одна наносекунда.

00:13:34 Упреждение и префетчер

  • Процессор может предзагружать данные в кэш, наблюдая за паттернами доступа.
  • Префетчер анализирует последовательные чтения и загружает следующий кэш-лайн заранее.
  • Это позволяет избежать кэш-мисс и обеспечить максимально быстрый доступ к данным.

00:15:04 Анализ структуры данных

  • Компилятор добавляет выравнивание и паддинг для оптимизации структуры данных.
  • Пример с структурой, которая изначально занимает 24 байта, а после оптимизации — 16 байт.
  • Оптимизация структуры приводит к ускорению кода благодаря использованию кэша.

00:18:45 Рекомендации по упаковке структур

  • Для максимальной оптимизации структуры должны быть упакованы так, чтобы сверху был максимально большой элемент.
  • Указатели следует размещать вверху структуры для оптимального размера.
  • Пример на Rust показывает, что компилятор может автоматически оптимизировать структуру.

00:21:57 Эксперимент с Rust и Go

  • Сравнение скорости выполнения кода на Rust и Go с правильно упакованными структурами.
  • Rust показывает значительно более высокую скорость выполнения.
  • Подчёркивается важность правильной упаковки структур для максимальной производительности.

00:22:54 Заключение

  • Призыв к зрителям оставить комментарии и подписаться на канал.
  • Обещание рассмотреть больше возможностей процессора в следующих видео.

В этом видео

Вступление. Пример программы и первый замер
0:02
Так, коллеги, давайте посмотрим пример
0:03
программы на Гошке. Смотрите, тут мы
0:06
заводим константу 100 млн. И вот
0:08
определяем структуру из трёх полей. И вы
0:11
сразу же меня спросите: «Почему на
0:12
Гошке-то? Почему не на Расте?» Но я вам
0:14
скажу, что на Расте это будет примерно
0:16
то же самое выглядеть, как и на Гошке.
0:19
Только будет единственная разница, на
0:21
Расте программа будет работать миллиард
0:23
раз быстрее, потому что раз — это blzing
0:25
fast. Но тем не менее пока суть не в
0:28
этом. Нам нужно понять, что здесь
0:30
происходит. Смотрите, константа.
0:32
Смотрите, структура. Смотрите, мы
0:35
создаём слайс. Вот тут мы делаем
0:37
бла-бла-бла. Вот тут
0:39
засекаем какое-то время. Тут просто
0:41
бежим по этому слайсу и складываем
0:43
значение поле Б у этих структур и
0:47
выводим на печать. Время, которое нам
0:49
понадобилось, чтобы пробежать миллион
0:52
100 млн структур и сумму, чтобы
0:54
компилятор не выпилил. Всё, давайте
0:56
проверять, сколько времени займёт запуск
0:59
этой программы. Запускается Goun. Так,
1:02
и, соответственно, первый запуск 805
1:04
мскунд. Почему же первый запуск такой
1:07
медленный, а следующий гораздо быстрее?
1:10
Какие варианты, коллеги? Но тем не
1:11
менее, смотрите, вот 112, 96 и 106. Ну,
1:16
скажем, что, допустим, 100 с чем-то
1:18
миллисекунд среднее. Идём обратно в код.
1:21
И что мы здесь хотим сделать? Поднимемся
Просто переставили поле → ускорение в 2 раза
1:24
вот сюда. Давайте в структуре фу поле а
1:27
опустим сверху вниз и всё. Больше
1:30
никаких изменений делать не будем.
1:32
Запускаем код 52 мскунды. 55.
1:36
Получается, мы просто переместили поле
1:40
внутри структуры и ускорили код в два
1:43
раза. Что это такое? Давайте
1:45
разбираться. чтобы разобраться, что же
1:48
произошло и почему код ускорился в два
1:50
раза. Давайте для начала посмотрим на
1:52
опять нашу любимую картинку стек и кучи
Стек и куча — это всё ещё медленная память!
1:55
и вообще, что такое оперативная память.
1:57
Мы уже обсуждали, что у нас есть стек,
1:59
есть куча, и мы можем переменные
2:01
выделять либо на стеке, либо на куче. И,
2:04
собственно, на стеке будет гораздо
2:06
быстрее, потому что он более компактный
2:08
и вообще там не бывает фрагментации и
2:10
прочего говна. не нужен локатор. То есть
2:12
процессор просто в одним регистром может
2:14
знать, где находится вершина стека и
2:17
оттуда брать данные. Но мы не обсудили
2:19
кое-что, ведь и стек, и куча находятся в
2:23
оперативной памяти. Ну, физически они
2:25
находятся в оперативной памяти. И как же
2:28
тогда, а, во-первых, возникает разница в
2:30
скорости, а, во-вторых, почему вообще
2:32
оно работает быстро, и нужно с этим
2:36
разобраться. Для начала давайте поймём,
2:38
что оперативная память, она работает
2:40
небыстро. Изначально 100 млн лет назад
2:43
ЦПУ и память оперативная работали
2:46
примерно одинаково. Но потом прошло 100
2:48
млн лет, и ЦПУ ускорился в 100 млрд раз,
2:52
соответственно,
2:53
100 млн триллирдов. И, короче, стал он
2:57
вообще экстремально быстрым. А память
2:59
как была медленной, так и осталась
3:01
медленной. И как же тогда наш процессор
3:04
вообще работает с такой медленной
3:06
штукой, как оперативная память?
3:08
Во-первых, давайте поймём, как он с ней
Как CPU общается с оперативной памятью
3:10
вообще вообще работает. Вот смотрите, у
3:12
него есть шина, некоторая системная,
3:15
шина ФСБ. И вот он через неё работает с
3:18
памятью. Шина — это, кстати, она очень
3:21
медленное звено, и она такая же
3:23
медленная, как и память. Ну, в принципе,
3:25
они одинаково медленные. И вот на этой
3:27
же шине может сидеть ещё какая-нибудь
3:29
сетевая карта. и, допустим, использовать
3:32
direct memory access и писать напрямую в
3:35
оперативную память. Ну, тоже как бы не
3:37
совсем напрямую, через эту же шину оно
3:40
будет писать в память. И что тогда
3:42
получается? Что, чтобы нам процессор
3:44
прочитал что-то из памяти, он должен
3:48
пойти на эту шину и сказать: «Шина,
3:51
свободна ли ты, а шина будет занята,
3:53
потому что в неё пишет сетевая карта». И
3:55
вообще ЦПУ у нас состоит не из одного
3:58
ядра, а из 100 млн ядер в данный момент.
4:01
Ну, допустим, 10 или 20. И они все могут
4:04
идти в оперативку и пытаться получить
4:06
какие-то данные оттуда. Соответственно,
4:08
вот здесь будет затык в этой шине. И не
4:10
только здесь, он будет вообще везде. И
4:13
оперативная память тоже работает
4:15
критически, медленно благодаря своей
4:17
вообще структуре. Поэтому нам нужно
4:19
понять, как как ЦПУ работает
4:23
экстремально быстро в условиях, когда
4:25
всё вокруг экстремально медленное. Чтобы
4:28
разобраться, как же ЦПУ удаётся так
Как устроен CPU и кеш L1 и L2
4:30
быстро работать, давайте посмотрим, что
4:32
же такое ЦПУ, что под под капотом, так
4:36
сказать. Ну, смотрите, во-первых, тут у
4:39
нас изображено два ядра. На самом деле
4:41
ядер гораздо больше, но чтобы понять, и
4:43
так видно, очевидно связь. Смотрите
4:47
дальше. Самое главное, что нам сейчас
4:49
нужно — это вот эти вот прямоугольники,
4:51
которые подписаны L1 и L2. Это кэш ЦПУ.
4:57
Есть также L3 у некоторых моделей. И вот
5:00
благодаря этому процессор работает
5:04
невероятно быстро. Как же оно так
5:07
устроено? Смотрите, во-первых, он ээ
5:10
ядро, когда хочет сложить какие-то
5:12
операции, провести над значениями,
5:14
которые есть в оперативке, он не идёт в
5:16
оперативную память, он проверяет, есть
5:18
ли они в
5:20
L1D. L1D — это кэш первого уровня для
5:25
данных. L1 дата. Есть L1 — это кэш
5:29
первого уровня для инструкций. Это как
5:32
раз-таки тот код на Расте, который
5:34
сейчас в данный момент запущен. И,
5:35
соответственно, наше ядро, оно, у него
5:38
есть и кэш инструкций, и кэш данных, и,
5:41
соответственно, оно крайне экстремально
5:43
быстро выполняет весь код. Если нету
5:47
кэш, если нету данных в уровне L1D, то
5:51
оно проверяет в уровне L2. И если его и
5:54
там нет, то тогда приходится идти в
5:57
оперативную память через шину. И это
6:00
ужасающе медленный процесс. Также мы тут
6:03
видим, что ядро один. И ядро 2 обладает
6:06
своими, каждый своим уровнем кэша L1.
6:10
Вот смотрите, L1D, L1. У ядра один он
6:13
свой, у ядра 2 он свой, но L2 у них
6:17
общий. Так работает примерно у всех ARM
6:21
процессоров, но, возможно, также оно
6:23
работает и у X86 процессоров. Это надо
6:27
проверять. Давайте посмотрим, какие
Сколько тактов стоит доступ к кешу и памяти
6:30
тайминги у ядра, чтобы прочитать что-то
6:32
из кэша первого уровня или же из кэша
6:35
второго уровня, ну или из оперативной
6:38
памяти. Итак, замерять будем в циклах
6:40
работы, в тактах. Такт — это единица
6:43
работы, которою он базовую операцию
6:45
какую-то выполнит. И, допустим,
6:47
процессор, у него частота 3 ГГц. Это
6:50
значит, что он 3 млрд тактов в секунду
6:53
выполнит. И, значит, один такт — это 0,3
6:56
наносекунды для данной частоты. Ну,
6:58
смотрите, давайте будем писать. Так,
7:01
вот, чтобы вычитать ядру из кэша первого
7:04
уровня, это три такта. Так и запишем.
7:07
Цуклес, это быстро. Три такта по 0,3
7:12
наносекунды. Это получается 1
7:15
наносекунда на то, чтобы вычитать
7:17
информацию. Кстати, чтобы взять из
7:19
регистра что-то или какую-то операцию с
7:21
регистром провернуть, то это нужен один
7:23
такт. Это невероятно быстро. Вычитать из
7:26
кэша первого уровня — это тоже
7:28
экстремально быстро. Дальше смотрим кэш
7:30
второго уровня. Кэш второго уровня — это
7:33
уже примерно 14 тактов процессору нужно
7:36
потратить. И это уже накладненька. Это 4
7:41
наносекунды, чтобы прочитать отсюда
7:43
информацию. Это уже надо напрячься и
7:47
подумать, нужно ли нам это, чтобы
7:49
сходить в оперативную память.
7:53
Это
7:55
240 тактов. Вы понимаете, это конец
7:58
света. Это так медленно, что это
8:02
невозможно вообще работать. 240 циклов —
8:05
это примерно 100
8:08
наносекунд. Если у нас программа будет
8:10
так долго ходить за
8:12
данными, то всё,
8:14
расходимся. Но не в мою
8:17
смену. Продолжаем погружение. Чтобы
8:20
понять, почему вот эта память, кэш
Что такое SRAM и DRAM (погружаемся в физику!)
8:22
память невероятно быстрая, а оперативная
8:25
память невероятно медленная, нам
8:27
придётся погрузиться ещё глубже,
8:29
практически на самое днищее. Давайте
8:31
погружаться. Смотрите, что мы имеем в
8:34
виду. Вот тут две ячейки памяти. Одна
8:37
static RAM, сRМ называется, и другая
8:40
динамическая память DRAM.
8:43
Как вы думаете, какая из них
8:45
используется на уровне кэша процессора,
8:48
а какая в планках оперативной памяти?
8:51
Ну, давайте посмотрим. Во-первых,
8:54
static RAM сR. Тут у нас 1 и3 и 2 и4
9:00
образуют такие инверторы, которые в
9:03
сумме дают нам вот такую ячейку триггер
9:06
Flipфлоop, который постоянно держит
9:08
значение либо ноль, либо один. Никакие
9:11
другие ему штуки не нужны. Он постоянно
9:13
статически держит значение. Ну и 5, и М6
9:17
нужны, чтобы считывать или записывать
9:19
данные. Рядом тот же вариант, э,
9:21
динамическая память. Здесь у нас в
9:24
качестве хранилища информации выступает
9:27
конденсатор или же ёмкость. Она либо
9:30
заряжена, либо нет. Так вот, чтобы
9:32
понять там ноль или единица, единица,
9:36
если она заряжена, ноль. Если нет, то
9:38
нам нужно подать напряжение вот сюда,
9:41
открыть вот этот транзистор и узнать,
9:43
какое там записано значение. Но, как вы
9:46
понимаете, когда мы откроем вот этот
9:49
транзистор, конденсатор соединится с
9:51
Data Lines вот этим вот, и заряд весь
9:53
утечёт. Это разрушающее чтение.
9:57
Получается, мы узнали, что там было, но
9:59
мы уничтожили информацию.
10:01
Соответственно, после этого нам нужно
10:03
сделать рефреш этой ячейки и заново
10:06
записать информацию, которая там была.
10:08
Так, после каждого чтения. И помимо
10:11
этого конденсатор также имеет свойство
10:14
разряжаться со временем. И каждые 64
10:17
миссекунды надо все вот такие ячейки
10:20
обновлять и делать им рефреш,
10:22
чтобы информация не протухла. И также
10:26
ещё, когда у нас транзистор, он всегда
10:28
держит какое-то значение вот в этой
10:30
ячейке. Конденсатор он не разряжается
10:33
мгновенно. Он имеет какую-то задержку
10:36
физическую в тайминге для того, чтобы
10:39
разрядиться и для того, чтобы вот этот
10:41
даталай увидел изменение сигнала.
10:44
Соответственно, вот drраam, она, в
10:46
принципе, работает медленнее из-за всех
10:47
вот этих ограничений, но у неё есть
10:49
плюсы. Видите, какая она маленькая.
10:51
Соответственно, она стоит гораздо
10:53
дешевле. Вот здесь вот. шесть
10:56
транзисторов, а вот тут только один
10:58
транзистор и одна ёмкость. Конечно же,
11:00
эта штука дешевле, она меньше места
11:02
занимает. И поэтому оперативная память у
11:05
нас гигабайты, а кши — это мегабайты,
11:09
потому что невыгодно вот такие штуки
11:11
гигабайтами делать. Соответственно,
11:14
dram это у нас вот эта вот оперативка —
11:19
это DRAM, а кши — это у нас сRAM. И
11:23
имеем отсюда такую разницу, что кыши
11:26
невероятно блейзинг фастовые, а
11:29
оперативка невероятно медленная, потому
11:31
что конденсатор, пока он там зарядится,
11:33
пока он разрядится, уже всё, программа
11:36
закроется и Windows взорвётся. Так, ну
11:38
что, мы уже поняли, что оперативная
Что такое cache line и как она загружается
11:41
память — это невероятно медленно. И если
11:43
бы не было кышей, то всё, пиши пропало.
11:46
Это бы то же самое, что и пожизненно
11:48
писать на голо такая же скорость у всех
11:50
приложений. Ну так вот, смотрите. А что
11:53
на самом деле, давайте посмотрим, что
11:54
такое кэш. Это не просто кэш каких-то
11:56
элементов. Здесь он хранит кэш-линиями,
11:59
так называемыми, память. Поэтому, когда
12:02
ядру нужно скачать какой-то элемент из
12:04
памяти к себе, то, ну, не скачать, а
12:07
загрузить, то он грузит его целую
12:10
кэш-линию. Например, вот нужен был
12:12
элемент по адресу X. Проверяется в кэше,
12:15
его здесь нет. Это кэш MIS, проверяется
12:18
L2, в нём тоже нет. И тогда мы вынуждены
12:21
грузить из оперативной памяти. И в этот
12:23
момент ядро загрузит целую кэш-линию.
12:27
Что это примерно означает? Вот,
12:28
допустим, у нас есть адрес X, по
12:30
которому лежит элемент пятёрочка, и по
12:33
адресу X + 1 восьмёрка, четвёрка, ну, и
12:35
так далее, да? И мы грузим целую
12:37
кэшлинию. Это значит, что от адреса X
12:40
ещё прибавятся все вот эти элементы,
12:43
которые входят в кэш-линию. То есть
12:45
процессор не загрузит только один вот
12:48
этот элемент, он загрузит все вот эти
12:50
вот элементы. Размер кэш-лини разный.
12:53
Порой 64 байта, порой 128. У меня,
12:57
соответственно, на компьютере на этом
12:59
Mac 128 байт размер кэш-линии. Ну так
13:03
вот, значит, если, как вы уже поняли,
13:06
если у нас данные лежат в массиве, там,
13:09
в слайсе плотненько рядом, и нам
13:11
процессор хочет что-то получить, элемент
13:13
по адресу X, то он загрузит целую
13:16
каш-линию. И если это был слайс, то он
13:19
загрузит и все следующие элементы
13:20
слайса, которые вошли в этот кэшлайн. И,
13:22
соответственно, когда мы захотим
13:24
какое-то действие выполнить со следующим
13:26
элементом, то он уже будет в кэше. И это
13:29
будет кэшхит, и это будет 1 наносекунда
13:32
на доступ. Это будет невероятно быстро.
13:34
Таким вот образом процессор загружает
13:37
данные из оперативки к себе в кэш. Но и
Как работает prefetch
13:40
это ещё не всё. Процессор не просто
13:42
ждёт, когда мы попросим элемент по
13:44
какому-то адресу и загрузит кэш-линию.
13:46
Он может работать на упреждение, как
13:49
говорится. Если предзагрузка неизбежна,
13:52
то предзагружая всё. Така такая тактика.
13:55
Смотрите, вот пример. Нам нужно
13:57
загрузить элемент по адресу X. Конечно
13:59
же, процессор берёт и загружает весь вот
14:02
этот кэшлайн. Дальше мы в цикле идём.
14:04
Обрабатали элемент по адресу X, перешли
14:07
на следующую восьмёрочку, с ней что-то
14:09
сделали, потом четвёрочку, с ней что-то
14:11
сделали. И, соответственно, при фейчер
14:13
хардваере видит, наблюдает некоторый
14:15
паттерн доступа к памяти. Он видит, что
14:17
мы просто идём последовательно и читаем
14:19
элементы. Он, естественно, не дурак. И
14:22
сразу же на упреждение загружает
14:24
следующий кэшйн. Таким образом, мы вот
14:27
здесь идём, и у нас всё время идёт
14:28
кэшitт. Это 1 наносекунда доступа. И
14:31
когда мы переходим вот эту границу, у
14:33
нас не случается кэшмис. У нас опять
14:36
здесь уже данные в кэше L1D. И,
14:40
соответственно, опять кэшхит и опять
14:42
максимально блейзинговый код. Таким
14:44
образом, вот, используя такие механизмы
14:47
шлайн и префейр, процессор делает наш
14:50
код. даже если он не очень блейзинговый,
14:53
он он его блейзит просто максимально.
Разбор примера кода из начала
14:55
Давайте вернёмся к коду изначала видео и
14:58
посмотрим, почему же код ускорился в
15:00
полтора раза, что же произошло или даже
15:03
в два раза. Давайте посмотрим. Для
15:05
начала посмотрим на код. Вот наша
15:07
структура фу, которая была, но это уже
15:10
она более такая выровненная. Напечатаем
15:12
её размер и поймём, сколько же байт она
15:14
занимает. Для этого можно использовать
15:16
библиотеку Unsave и её метод size off.
15:19
Но мы также можем и руками посчитать.
15:22
Давайте посмотрим. Вот такой размер
15:24
структуры был изначально. Вот это 1 бай,
15:27
вот это 8 байт. Да. Да. И вот это у нас
15:33
2 байта. Но мы не можем просто посчитать
15:36
1 + 8 + 2. Это получается 8 + 2 10 + 1
15:41

Но структура должна быть выровнена.
15:44
То есть вот смотрите, между вот этим и
15:45
этим не может быть вот это поле не может
15:48
начинаться сразу с байта с номером два.
15:51
Соответственно, здесь будет 1 + 7. 7
15:53
байт выравнивание вставит компилятор,
15:55
чтобы поля сравнялись вот с этим полем.
15:58
Получается теперь вот этот размер вот
16:00
этой структуры, размер первого поля
16:02
будет 8 байт. Всё равно, несмотря на то,
16:05
что мы указали его 1 байт. 8 + 8 будет
16:08
16 и ещё + 2 18. Но вся структура должна
16:12
быть выровнена по вот этому полю. То
16:14
есть она должна начинаться ноль.
16:17
Следующая 16, следующая восемь, если
16:21
будет две структуры. Дальше ещё с
16:23
шестнадцатого адреса. Но если мы уже на
16:24
восемнадцатом адресе находимся, то
16:26
следующий адрес будет доступный нам,
16:28
чтобы структура началась — это 24.
16:31
Соответственно, вот сюда компилятор ещё
16:34
вставит ещё падинг. И это будет Что нам
16:37
тут капилот написал? Фиг знает. Падинг
16:40
будет 24 — 18 и это равняется 6 байт.
16:45
Ещё будет падинга. Итого всё равно вот
16:47
эта вся структура будет занимать 24
16:50
байта. Давайте проверять
16:52
гон. Вот смотрите, 24 байта и первый
16:56
запуск 780 мскунд. Я, помните, вам
16:59
говорил, почему же первый запуск
17:01
отличается от второго. Вот второй 157
17:04
мскунд. А потому что, насколько мы
17:06
поняли, все вот эти данные находятся уже
17:08
в кэше, и второй запуск берёт их просто
17:11
из кэша, а не из оперативки. Теперь
17:14
идёмте и поменяем поля местами. Вот это
17:18
мы удаляем и ставим в конец. И теперь
17:21
что же происходит? Смотрите, вот это 8
17:23
байт по-прежнему, вот это два и вот это
17:26
у нас один. Теперь ничего не нужно ни
17:29
докуда доращивать поля. Они все сверху
17:31
вниз по убыванию выстроены размеры.
17:34
Теперь нужно, чтобы вся структура
17:36
выравнивалась только по большому адресу,
17:38
то есть по восемь. У нас размер
17:40
структуры 11. 11 — это больше возьми.
17:42
Значит, следующий падинг следующее
17:44
выравнивание — это будет 16. Значит, она
17:47
добавит сюда падинг. 16 — 11 равняется
17:52
сколько там? 5 байт. Вставит вот сюда в
17:55
конец структуры нам компилятор. И общий
17:57
размер будет 16 байт, потому что у нас
18:01
вот это меньше ст. Давайте проверять.
18:05
Запускаем. Получаем, да, 16 байт. Ну и
18:07
время, конечно же, ускорилось. Ну и
18:10
почему это произошло? Теперь несложно
18:12
догадаться, что у нас размер кэшлинии
18:14

Если мы берём и загружаем структуры
18:18
размером по 24 байта, то мы всего можем
18:21
пять структур за раз загрузить. Если по
18:24
16 байт, то, соответственно, уже восемь
18:26
структур. И видите, они как плотненько
18:27
выровнены. И кошлинии прямо будут в
18:30
выравнивание попадать. Вот и,
18:32
собственно, это и есть ускорение. Плюс
18:34
префейчер и плюс всё прочее, ну и так
18:37
далее. И, соответственно, код мы просто
18:40
взяли и ускорили на ровном месте. Так.
18:42
Ну и давайте ещё немного базы по тому,
Как правильно упаковывать структуру
18:44
как паковать структуру. Смотрите, чтобы
18:46
код действительно блейзился, нам нужно
18:48
структуру паковать таким образом, чтобы
18:50
сверху вот всегда было максимально
18:53
большой элемент. Вот мы по убыванию
18:56
ставим сверху большой, дальше меньше,
18:58
меньше и так до самого низа. Если у нас
19:00
там какой-то поинтер и ещё прочее, то
19:03
известно всем, что в Гошке поинтер — это
19:06
шестьдесятчетырёхбитное значение на
19:08
системе, если она
19:09
шестьдесятчетырёхбитная. Соответственно,
19:10
это тоже будет восемь. Значит, поинтер
19:12
можно вставить сюда. Допустим, здесь
19:14
есть какой-то указатель next, и он будет
19:18
на фу тоже указывать, то мы его ставим
19:21
вверх, чтобы у нас размер структуры был
19:25
оптимальненький. Вот такие дела. Ну и
Rust: почему компилятор сам переставляет поля
19:27
давайте посмотрим этот же самый пример,
19:29
только на Расте, как я уже и говорил,
19:31
должно работать в миллиарды и миллионов
19:33
раз быстрее. Раз быстрее. Вот смотрите,
19:36
это же структура фу. Сверху у нас тут 1
19:39
байт, она неправильно упакована. 1 байт
19:41
здесь 8 байт здесь 2 байта. Дальше у нас
19:45
100 млн константа. И дальше бла-бла-бла.
19:48
И выводим время. Давайте проверять, как
19:50
это работает. Для начала надо сбил
19:52
структуру Cargo buildреase, чтобы версия
19:56
с оптимизациями была готова. Ну и
19:58
запускаем Tarгет. Первый запуск 82 мску,
20:02

Ну, скажем, 80-90 мскунд. Всё-таки
20:06
здесь идёт запись и всё прочее. Значение
20:08
не очень верные. Поднимемся сюда в
20:10
структуру и опустим поле А вниз. Будет
20:14
ли ускорение в два раза? Cargo
20:16
buildреas. Так, 130, 96, 97, 89, 86. Как
20:22
мы видим, ничего не изменилось, потому
20:25
что расту на самом деле пофиг, как у нас
20:28
вот здесь вот внутри находятся поля, он
20:30
их выстраивает в момент компиляции так,
20:32
чтобы оно занимало меньше места.
20:34
Соответственно, когда у нас было так, в
20:36
момент компиляции он выставит вот так,
20:39
структура будет занимать меньше места и
20:41
будет более блейзинговый код. Чтобы
20:43
попросить Раст не делать этого, есть вот
20:45
такая
20:46
директива C. Это значит, раст,
20:50
пожалуйста, не меняй местами поля внутри
20:53
структуры. Именно си. Для того, чтобы
20:56
передавать, допустим, связываться как-то
20:57
с сишним кодом, вот этот порядок полей
21:00
должен быть строго определён. По
21:01
умолчанию Rust используется, и он может
21:04
менять поля. Ну вот давайте зафиксируем
21:06
неправильный неправильную упаковку
21:08
структуры Full. Мы написали C и написали
21:12
А наверху
21:14
запускаем так: 144, 149, 120, 116. Ну
21:20
вот, меньше сотки не опускается,
21:23
несмотря на то, что раз невероятно
21:24
быстрый. Теперь опустим вот эту штуку
21:27
вниз. Билдим. Ещё раз запустим. 63 65
21:32

Вот оно. Ускорение в два раза. Но
21:36
тем не менее, если мы сейчас снесём
21:40
C, то, в принципе, результат будет такой
21:43
же. 68 75 68. Таким образом, нам сам
21:48
подблейзивает код. Помимо всех прочих
21:50
своих оптимизаций, его невероятно умный
21:52
компилятор ещё и нам структуры делает
21:55
более правильными и выровненными. Ну и
21:57
давайте ещё один эксперимент поставим.
Разница между Go и Rust по скорости
22:00
Вот у нас код на Расте. Вот у нас код на
22:04
Гошке. два одинаковых кода. Как вы
22:07
видите, здесь упакована структура фу
22:10
правильно, 64168, то есть максимально
22:12
блейзинговый код будет. Давайте
22:14
проверять, кто быстрее. Чтобы сбилдить
22:17
экзешнчик на GO, просто команда go
22:19
build, и она уже включает все
22:21
максимальные оптимизации для
22:22
максимальной блейзинговости. И она
22:24
билдит экзешник за 0 наномикросекунд.
22:29
90 86 92 и тот же
22:35
раст 8075. Итого мы видим, что на расте
22:39
75 мсекунд, а на гошке минимальный
22:43
результат 86. Получается раст опять в 3
22:47
триллиарда раз 3 триллиарда раст
22:52
быстрее, чем Гошка. Ну что ж, коллеги,
Завершение
22:53
на этом всё. И теперь просьба для тех,
22:56
кто досмотрел досюда. Пожалуйста,
22:59
напишите в комментариях ответ на
23:00
следующий вопрос. Хотите ли вы, чтобы мы
23:04
что-то постримили вместе? Это раз.
23:06
Во-вторых, заводим ли группу в
23:08
Телеграме? Вот такие два вопроса,
23:10
пожалуйста, ответьте, кто досмотрел
23:12
досюда. Ну и на этом всё. Если
23:15
понравилось, подписывайтесь на канал. В
23:17
следующем видео посмотрим больше
23:19
моментов, как можно использовать эти
23:21
возможности процессора, чтобы писать
23:24
максимально блейзинговый и casшfendly
23:27
код и получать максимальные профиты от
23:30
использования от дружбы с
23:33
процессором. На этом всё. Всем пока.

Прокрутить вверх