Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.
По всем вопросам (+ реклама) @ninjatelegramm
Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Post #1076
3.63K
Loop unrolling. Быстрая обработка хвостов
#новичкам
В прошлый раз мы пришли к важной мысли: нам нужно обрабатывать хвосты развернутых циклов и делать это быстро.
В С++ коде пока непонятно, как это сделать. Но в ассемблере есть довольно старая техника для этого.
Возьмем С++ код:
В целом, мы хотим, чтобы хвосты обрабатывались примерно так:
Только без кучи повторяющегося кода, количество которого будет только увеличиваться с увеличением фактора разворачивания цикла.
Идея: на месте пролога напишем 4 инструкции присваивания, а между ними поставим метки. В начале мы узнаем остаток и прыгаем на нужную метку. После чего раз за разом проваливаемся в следующую метку и в итоге в развернутый цикл. То есть в начале обрабатываем хвост правильного размера с помощью меток, а потом уже попадаем в основной цикл. В переводе на С++ это выглядит так:
Выглядит адски: свитч, какие-то метки, на которых одинаковый код, goto. За такое сразу бы запретили человеку коммитить код, только документацию писать отныне и навсегда. Поэтому никто так не пишет.
Зато эту лапшу можно спрятать в скомпилированном ассемблере, который вряд ли кто-то будет читать. Компилятор превращает исходный С++ код из начала поста в ассемблер примерно такого же вида, как последний сниппет. Вот примерчик на годболте как это выглядит в оригинале.
Hide your impurities deep. Stay cool.
#performance #compiler
#новичкам
В прошлый раз мы пришли к важной мысли: нам нужно обрабатывать хвосты развернутых циклов и делать это быстро.
В С++ коде пока непонятно, как это сделать. Но в ассемблере есть довольно старая техника для этого.
Возьмем С++ код:
void output_squares(size_t count, size_t *output) {
for (size_t i = 0; i < count; ++i) {
*output++ = i * i;
}
}В целом, мы хотим, чтобы хвосты обрабатывались примерно так:
#include <cstddef>
void output_squares(size_t count, size_t *output) {
size_t i = 0;
size_t remainder = count % 4;
// Эта часть назвается пролог
if (remainder == 3) {
output[2] = 2 * 2;
output[1] = 1 * 1;
output[0] = 0 * 0;
i = 3;
} else if (remainder == 2) {
output[1] = 1 * 1;
output[0] = 0 * 0;
i = 2;
} else if (remainder == 1) {
output[0] = 0 * 0;
i = 1;
}
for (; i < count; i += 4) {
output[i] = i * i;
output[i + 1] = (i + 1) * (i + 1);
output[i + 2] = (i + 2) * (i + 2);
output[i + 3] = (i + 3) * (i + 3);
}
}
Только без кучи повторяющегося кода, количество которого будет только увеличиваться с увеличением фактора разворачивания цикла.
Идея: на месте пролога напишем 4 инструкции присваивания, а между ними поставим метки. В начале мы узнаем остаток и прыгаем на нужную метку. После чего раз за разом проваливаемся в следующую метку и в итоге в развернутый цикл. То есть в начале обрабатываем хвост правильного размера с помощью меток, а потом уже попадаем в основной цикл. В переводе на С++ это выглядит так:
void output_squares(size_t count, size_t *output) {
size_t i = 0;
size_t r = count % 4;
switch (r) {
case 3: goto rest3;
case 2: goto rest2;
case 1: goto rest1;
default: goto main_loop;
}
rest3:
output[i] = i * i; ++i;
rest2:
output[i] = i * i; ++i;
rest1:
output[i] = i * i; ++i;
main_loop:
for (; i < count; i += 4) {
output[i] = i * i;
output[i + 1] = (i + 1) * (i + 1);
output[i + 2] = (i + 2) * (i + 2);
output[i + 3] = (i + 3) * (i + 3);
}
}Выглядит адски: свитч, какие-то метки, на которых одинаковый код, goto. За такое сразу бы запретили человеку коммитить код, только документацию писать отныне и навсегда. Поэтому никто так не пишет.
Зато эту лапшу можно спрятать в скомпилированном ассемблере, который вряд ли кто-то будет читать. Компилятор превращает исходный С++ код из начала поста в ассемблер примерно такого же вида, как последний сниппет. Вот примерчик на годболте как это выглядит в оригинале.
Hide your impurities deep. Stay cool.
#performance #compiler
- 🔥 15
- ❤ 10
- 👍 8
- 😁 2
- 🤯 1
