WAT
#опытным
Сколько будет х/х?
Как ни странно, на этот вопрос есть множество ответов.
Математический ответ: при x != 0 выражение вырождается в единицу. Если же x = 0, то значение выражения неопределено. x=0 приводит к неопределенности вида 0/0, у которого нет корректного значения.
Какой же ответ у С++?
Ответ будет 1!
На те вот ссылочку на годболт, если не верите.
#ЧЗХ? Компьютер считать чтоли не умеет?
По ссылочке на самом деле только вывод gcc показан. И он действительно выдает 1.
Но на кланге при запуске программа получает сигнал SIGFPE, а на msvc выдается вот такая цифирь 3221225620.
Понятное дело, что деление на 0 - это UB, и это явно видно по результатам запуска на разных компиляторах.
Если вы думали, что проблема проявляется только оптимизациях - нет. Даже на -O0 все равно gcc единичку выдает.
Тогда как могла появиться единичка?
С какой-то стороны этот вопрос не имеет смысла, потому что при UB компилятор волен поступать, как ему вздумается. Но этот конкретный пример показывает одну из популярных моделей поведения компилятора при UB.
С точки зрения разработчика С++ UB - это скорее плохо. Некоторые из нас идут на риск, когда понимают реальный эффект на конкретном компиляторе и железе, но в большинстве кейсов мы стараемся избегать UB.
И компилятор это учитывает. Если UB - это плохо и программист старается его не допускать в своем коде, значит можно считать, что UB в коде не будет. И на основе этого знания компилировать программу.
UB в данном случае произойдет только при x=0. Тогда компилятор просто выкидывает этот вариант из анализа, притворяется, что такого не может быть. Тогда выражение x/x действительно становится равным единице.
Компилятор применил константную свёртку (constant folding) — одно из базовых преобразований, которое работает даже на нулевом уровне оптимизации. Он увидел выражение
Чтобы компилятор поступал максимально честно и ничего не смог сделать с выражением, надо пометить переменную volatile.
Это ключевое слово запрещает компилятору делать предположения о значении переменной и заставляет чего честно читать переменную из памяти.
Do not trust programmers. Stay cool.
#compiler
#опытным
Сколько будет х/х?
Как ни странно, на этот вопрос есть множество ответов.
Математический ответ: при x != 0 выражение вырождается в единицу. Если же x = 0, то значение выражения неопределено. x=0 приводит к неопределенности вида 0/0, у которого нет корректного значения.
Какой же ответ у С++?
#include <iostream>
int main() {
int x = 0;
std::cout << x/x << std::endl;
}
Ответ будет 1!
На те вот ссылочку на годболт, если не верите.
#ЧЗХ? Компьютер считать чтоли не умеет?
По ссылочке на самом деле только вывод gcc показан. И он действительно выдает 1.
Но на кланге при запуске программа получает сигнал SIGFPE, а на msvc выдается вот такая цифирь 3221225620.
Понятное дело, что деление на 0 - это UB, и это явно видно по результатам запуска на разных компиляторах.
Если вы думали, что проблема проявляется только оптимизациях - нет. Даже на -O0 все равно gcc единичку выдает.
Тогда как могла появиться единичка?
С какой-то стороны этот вопрос не имеет смысла, потому что при UB компилятор волен поступать, как ему вздумается. Но этот конкретный пример показывает одну из популярных моделей поведения компилятора при UB.
С точки зрения разработчика С++ UB - это скорее плохо. Некоторые из нас идут на риск, когда понимают реальный эффект на конкретном компиляторе и железе, но в большинстве кейсов мы стараемся избегать UB.
И компилятор это учитывает. Если UB - это плохо и программист старается его не допускать в своем коде, значит можно считать, что UB в коде не будет. И на основе этого знания компилировать программу.
UB в данном случае произойдет только при x=0. Тогда компилятор просто выкидывает этот вариант из анализа, притворяется, что такого не может быть. Тогда выражение x/x действительно становится равным единице.
Компилятор применил константную свёртку (constant folding) — одно из базовых преобразований, которое работает даже на нулевом уровне оптимизации. Он увидел выражение
x / x, где x — целочисленная переменная, и, как мы все делали в школе, сократил числитель и знаменатель. В итоге на консоль вывелась единичка.Чтобы компилятор поступал максимально честно и ничего не смог сделать с выражением, надо пометить переменную volatile.
volatile int x = 0;
std::cout << x/x << std::endl;
Это ключевое слово запрещает компилятору делать предположения о значении переменной и заставляет чего честно читать переменную из памяти.
Do not trust programmers. Stay cool.
#compiler
🔥24❤14👍9
Loop unrolling. Хвосты
#новичкам
Предыдущий пост тут.
Следующие пару постов могут быть немного скучными и замудренными, но темы по смыслу именно так бьются. Разговор о хвостах нужен, чтобы правильно понять одну технику(спойлер: Duff device)
Удобненько получилось, что в прошлом посте число итераций цикла делилось на 4. 1024 % 4 = 0.
Но как разворачивать циклы, если число итераций не делится нацело на фактор развертывания(количество повторов операции в развертке)?
Да тем же циклом. Обрабатываем целую часть развернутым циклом и потом дорабатываем остаток:
Последний цикл - это и есть дообработка хвоста.
И обработка хвостов - это важная штука, которую нельзя забывать хоть при разворачивании цикла, хоть при векторизации вычислений, хоть при распараллеливании вычислений.
Чем вообще плох хвост? Мы заранее не знаем его размер и поэтому вынуждены использовать привычный неразвернутый цикл для его обработки. Это опять же приносит с собой недостатки обычных циклов.
Если бы мы только знали, сколько элементов в хвосте коллекции, то смогли бы и этот последний цикл развернуть просто в набор операций.
И есть несколько техник, которые позволяют сэмулировать нечто подобное. О них мы поговорим в следующие разы, они отдельных постов заслуживают.
See the job through. Stay cool.
#performance
#новичкам
Предыдущий пост тут.
Следующие пару постов могут быть немного скучными и замудренными, но темы по смыслу именно так бьются. Разговор о хвостах нужен, чтобы правильно понять одну технику(спойлер: Duff device)
Удобненько получилось, что в прошлом посте число итераций цикла делилось на 4. 1024 % 4 = 0.
int sum = 0;
for (int i = 0; i < 1024; i += 4) {
sum += arr[i];
sum += arr[i+1];
sum += arr[i+2];
sum += arr[i+3];
}
Но как разворачивать циклы, если число итераций не делится нацело на фактор развертывания(количество повторов операции в развертке)?
Да тем же циклом. Обрабатываем целую часть развернутым циклом и потом дорабатываем остаток:
int sum = 0;
int i = 0;
int unroll_factor = 4;
int main_limit = n - n % unroll_factor;
for (; i < main_limit; i += unroll_factor) {
sum += arr[i];
sum += arr[i + 1];
sum += arr[i + 2];
sum += arr[i + 3];
}
// HERE
for (; i < n; ++i) {
sum += arr[i];
}
Последний цикл - это и есть дообработка хвоста.
И обработка хвостов - это важная штука, которую нельзя забывать хоть при разворачивании цикла, хоть при векторизации вычислений, хоть при распараллеливании вычислений.
Чем вообще плох хвост? Мы заранее не знаем его размер и поэтому вынуждены использовать привычный неразвернутый цикл для его обработки. Это опять же приносит с собой недостатки обычных циклов.
Если бы мы только знали, сколько элементов в хвосте коллекции, то смогли бы и этот последний цикл развернуть просто в набор операций.
И есть несколько техник, которые позволяют сэмулировать нечто подобное. О них мы поговорим в следующие разы, они отдельных постов заслуживают.
See the job through. Stay cool.
#performance
👍20❤10🔥6
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
🔥13❤10👍8😁2🤯1
Loop unrolling. Duff's device
#опытным
Если у вас чуть подвытекали глаза, когда вы увидели в прошлом посте эту схему со свитчами и гоуту, то сегодня они вытекут окончательно.
Если приглядеться в механику этого кода
То можно заметить одну вещь. Мы используем возможность беспретятственного прохождения исполнения сквозь метки при этом имеем возможность воткнуться в любой момент времени, перейдя по нужной метке.
Но эта механика же полностью закрывается функциональностью switch. Код может проходить от кейса к кейсу, пока не встретит break и изначально можно перейти в любой из кейсов.
Давайте сделаем трах-тебедох и представим код в другом виде:
Как это работает
👉🏿
👉🏿
👉🏿 Тело цикла содержит четыре одинаковых операции (вычисление квадрата и сдвиг индекса), размеченных метками
👉🏿 Цикл продолжается, пока
Первым такую особенность заметил мисье Том Дафф. В тот момент он работал в Lucasfilm и пытался оптимизировать копирование данных в memory-mapped регистр для анимации реального времени. Ему нужно было развернуть цикл для скорости, но была проблема — количество итераций не всегда кратно размеру развёртки.
Он вдохновился приемами из ассемблера и написал на языке K&R С(одна из ранних версий С, до стандартизации) вот такую конструкцию:
Функция send - это такой аналог memcpy, до внедрения в стандарт этой функции. Идея такая же: прыгаем в середину свитча для обработки остатка, а дальше крутимся в развернутом while, пока не скопируем все элементы.
С тех пор код такого вида называется Duff's device или устройство Даффа.
В 80-е техника давала буст производительности хоть и вызывала вопросы к читаемости кода и в целом к средствам языка, которые предоставляют такие возможности.
Но в современном мире не стоит даже и пытаться нечто такое написать в проде.Компиляторы cами отлично разворачивают циклы, часто лучше, чем ручная оптимизация.
И вообще перед применением любой оптимизации кода следует выполнить его бенчмаркинг или изучить сгенерированный компилятором вывод, чтобы убедиться, что код работает ожидаемым образом на целевой архитектуре, уровне оптимизации и компиляторе.
Тем не менее Duff's device остался в истории как пример вырвиглазной эксплуатации тонкостей языка в гонке за производительностью.
Be relevant. Stay cool.
#compiler #optimization #performance
#опытным
Если у вас чуть подвытекали глаза, когда вы увидели в прошлом посте эту схему со свитчами и гоуту, то сегодня они вытекут окончательно.
Если приглядеться в механику этого кода
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);
}То можно заметить одну вещь. Мы используем возможность беспретятственного прохождения исполнения сквозь метки при этом имеем возможность воткнуться в любой момент времени, перейдя по нужной метке.
Но эта механика же полностью закрывается функциональностью switch. Код может проходить от кейса к кейсу, пока не встретит break и изначально можно перейти в любой из кейсов.
Давайте сделаем трах-тебедох и представим код в другом виде:
if (count == 0) return;
size_t i = 0;
size_t n = (count + 3) / 4;
switch (count % 4) {
case 0:
do {
output[i] = i * i; i++;
case 3:
output[i] = i * i; i++;
case 2:
output[i] = i * i; i++;
case 1:
output[i] = i * i; i++;
} while (--n > 0);
}
Как это работает
👉🏿
n – число полных итераций развёрнутого цикла, необходимых для обработки всех элементов (с учётом возможного неполного первого прохода).👉🏿
switch (count % 4) выбирает точку входа в тело цикла do-while в зависимости от остатка. При остатке 0 начинаем с начала (полная итерация), при остатке 3 – с третьей операции и т.д.👉🏿 Тело цикла содержит четыре одинаковых операции (вычисление квадрата и сдвиг индекса), размеченных метками
case. За счёт проваливания (fall-through) первая (неполная) итерация выполняет ровно столько операций, сколько нужно для обработки хвоста, а все последующие итерации – по четыре операции.👉🏿 Цикл продолжается, пока
--n > 0, что гарантирует обработку всех элементов.Первым такую особенность заметил мисье Том Дафф. В тот момент он работал в Lucasfilm и пытался оптимизировать копирование данных в memory-mapped регистр для анимации реального времени. Ему нужно было развернуть цикл для скорости, но была проблема — количество итераций не всегда кратно размеру развёртки.
Он вдохновился приемами из ассемблера и написал на языке K&R С(одна из ранних версий С, до стандартизации) вот такую конструкцию:
send(to, from, count)
register short *to, *from;
register count;
{
// Begin of the function
register n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
Функция send - это такой аналог memcpy, до внедрения в стандарт этой функции. Идея такая же: прыгаем в середину свитча для обработки остатка, а дальше крутимся в развернутом while, пока не скопируем все элементы.
С тех пор код такого вида называется Duff's device или устройство Даффа.
В 80-е техника давала буст производительности хоть и вызывала вопросы к читаемости кода и в целом к средствам языка, которые предоставляют такие возможности.
Но в современном мире не стоит даже и пытаться нечто такое написать в проде.Компиляторы cами отлично разворачивают циклы, часто лучше, чем ручная оптимизация.
И вообще перед применением любой оптимизации кода следует выполнить его бенчмаркинг или изучить сгенерированный компилятором вывод, чтобы убедиться, что код работает ожидаемым образом на целевой архитектуре, уровне оптимизации и компиляторе.
Тем не менее Duff's device остался в истории как пример вырвиглазной эксплуатации тонкостей языка в гонке за производительностью.
Be relevant. Stay cool.
#compiler #optimization #performance
❤26👍12🔥5🤯2❤🔥1👏1
Loop unrolling. pragma unroll
#опытным
Компилятор конечно умный и сам умеет разворачивать циклы. Но делает он это не во всех случаях. Когда-то уровень оптимизации не позволяет(агрессивная развертка делается на O3) или количество итераций цикла неизвестно.
Если же вы хотите вручную контролировать развертку, но не писать код развертки руками - для вас есть один вариант. Это
Правда, есть загвоздка. Стандартного синтаксиса этой директивы не существует, для каждого компилятора чуть свой синтаксис:
-👉🏿GCC и Clang: Используется
👉🏿 MSVC (Microsoft Visual C++): здесь все, как не у всех: в msvc нет прямого аналога директивы
👉🏿 Intel и NVIDIA компиляторы: Используют
👉🏿 ARM компилятор: Поддерживает
Раз у всех все свое, то в примерах будет использовать некую усредненную версию.
По сути есть 2 возможных варианта использования директивы:
🙈 Полная развертка
В этом случае компилятор обязан знать число итераций цикла, чтобы полностью его развернуть.
Плюс чем больше итераций, тем сильнее распухает код бинаря.
Поэтому в данном виде директива встречается не так часто.
☃️ Частичная развертка
Это именно та история, которую мы обсуждаем всю серию статей.
Как прагма работает?
1️⃣ Препроцессор оставляет
2️⃣ На этапе семантического анализа компилятор встречает
3️⃣ Специальная сущность в компиляторе, отвечающая за развертку циклов, слушает и повинуется прагме и делает столько операций в цикле, сколько сказал разработчик. Плюс добавляет обработку хвоста.
Здесь можно посмотреть примерчики с gcc-шной версией прагмы.
Напомню, что компилятор сам хорошо умеет оптимизировать циклы, особенно на O3. Используйте прагму только тогда, когда вы сами ручками проверили, что компилятор в вашем случае бессилен или делает что-то неправильно.
Плюс обязательно делайте перфоманс тестирование, чтобы выявить действительно лучший по производительности вариант.
Measure your performance. Stay cool.
#compiler #optimization #performance
#опытным
Компилятор конечно умный и сам умеет разворачивать циклы. Но делает он это не во всех случаях. Когда-то уровень оптимизации не позволяет(агрессивная развертка делается на O3) или количество итераций цикла неизвестно.
Если же вы хотите вручную контролировать развертку, но не писать код развертки руками - для вас есть один вариант. Это
#pragma unroll.Правда, есть загвоздка. Стандартного синтаксиса этой директивы не существует, для каждого компилятора чуть свой синтаксис:
-👉🏿GCC и Clang: Используется
#pragma GCC unroll Для Clang существует более детализированный синтаксис #pragma clang loop unroll.👉🏿 MSVC (Microsoft Visual C++): здесь все, как не у всех: в msvc нет прямого аналога директивы
#pragma unroll. Есть #pragma loop, но она контролирует скорее автовекторизацию, чем развертку.👉🏿 Intel и NVIDIA компиляторы: Используют
#pragma unroll.👉🏿 ARM компилятор: Поддерживает
#pragma unroll.Раз у всех все свое, то в примерах будет использовать некую усредненную версию.
По сути есть 2 возможных варианта использования директивы:
🙈 Полная развертка
#pragma unroll
for (int i = 0; i < 12; ++i) {
p1[i] += p2[i] * 2;
}
В этом случае компилятор обязан знать число итераций цикла, чтобы полностью его развернуть.
Плюс чем больше итераций, тем сильнее распухает код бинаря.
Поэтому в данном виде директива встречается не так часто.
☃️ Частичная развертка
Это именно та история, которую мы обсуждаем всю серию статей.
#pragma unroll N позволяет развернуть цикл так, чтобы за одну итерацию тело цикла выполнялось N раз.#pragma GCC unroll 4
for (size_t i = 0; i < n; ++i) {
sum += arr[i];
}
Как прагма работает?
1️⃣ Препроцессор оставляет
#pragma unroll после препроцессинга в единице трансляции для того, чтобы компилятор смог ее учесть.2️⃣ На этапе семантического анализа компилятор встречает
#pragma unroll и связывает его со следующим за ним циклом.3️⃣ Специальная сущность в компиляторе, отвечающая за развертку циклов, слушает и повинуется прагме и делает столько операций в цикле, сколько сказал разработчик. Плюс добавляет обработку хвоста.
Здесь можно посмотреть примерчики с gcc-шной версией прагмы.
Напомню, что компилятор сам хорошо умеет оптимизировать циклы, особенно на O3. Используйте прагму только тогда, когда вы сами ручками проверили, что компилятор в вашем случае бессилен или делает что-то неправильно.
Плюс обязательно делайте перфоманс тестирование, чтобы выявить действительно лучший по производительности вариант.
Measure your performance. Stay cool.
#compiler #optimization #performance
❤23👍10🔥3😁3👏1
Сколько раз можно разыменовать лямбду?
#опытным
Давайте для начала поговорим о том, сколько раз можно разыменовать указатель?
Зависит от того, какой указатель мы имеем ввиду.
Тип
Но мы конечно же можем определить указатель на указатель и добиваться очень глубоких уровней индирекции. Однако мы все равно сможем разыменовать такой указатель ровно по количеству этих уровней, не больше:
Сколько же раз можно разыменовывать лямбду?
Так стоп. Как в лямбде можно применять оператор?
Лямбда без захвата неявно кастится к указателю на функцию. А указатель можно разыменовывать.
Так сколько?
Бесконечно
Чисто технически мы конечно ограничены количеством атомов во вселенной или, что более реально, возможностями компилятора обрабатывать длиннющие тексты программ. Но формальных ограничений нет.
#ЧЗХ?
Давайте подумаем, что происходит при разыменовании лямбды. Она приводится к указателю на функцию, оператор применяется и результатом мы получаем lvalue функции(саму функцию, а не указатель). Тип этого выражения –
А функции у нас что любят делать? Правильно, неявно преобразовываться к указателю на саму себя.
Поэтому можем применить оператор еще разик.
А потом еще и еще. И еще, и еще, и еще, и еще... Ну вы поняли.
В любом случае ret по итогам того же неявного преобразования будет иметь тип указателя на фукнцию:
По тем же рассуждениям, кстати, любой указатель на функцию тоже можно бесконечно разыменовать.
Спасибо, @Ivaneo, за любезно предоставленный примерчик.
Be limited only by your imagination. Stay cool.
#cppcore
#опытным
Давайте для начала поговорим о том, сколько раз можно разыменовать указатель?
Зависит от того, какой указатель мы имеем ввиду.
Тип
int* можно разыменовать всего один раз. Получившийся объект после разыменования будет lvalue int, для которого отсутствует перегрузка operator*:int i = 5;
int * p = &i;
std::cout << **p << std::endl; // error: indirection requires pointer operand ('int' invalid)
Но мы конечно же можем определить указатель на указатель и добиваться очень глубоких уровней индирекции. Однако мы все равно сможем разыменовать такой указатель ровно по количеству этих уровней, не больше:
int i = 5;
int * p = &i;
int ** p1 = &p;
std::cout << **p1 << std::endl; // OK
std::cout << ***p1 << std::endl; // Error
Сколько же раз можно разыменовывать лямбду?
Так стоп. Как в лямбде можно применять оператор?
Лямбда без захвата неявно кастится к указателю на функцию. А указатель можно разыменовывать.
Так сколько?
Бесконечно
#include <iostream>
int main() {
auto ret = *****************************************[]{ return 23; };
std::cout << ret() << std::endl;
}
Чисто технически мы конечно ограничены количеством атомов во вселенной или, что более реально, возможностями компилятора обрабатывать длиннющие тексты программ. Но формальных ограничений нет.
#ЧЗХ?
Давайте подумаем, что происходит при разыменовании лямбды. Она приводится к указателю на функцию, оператор применяется и результатом мы получаем lvalue функции(саму функцию, а не указатель). Тип этого выражения –
int().А функции у нас что любят делать? Правильно, неявно преобразовываться к указателю на саму себя.
Поэтому можем применить оператор еще разик.
А потом еще и еще. И еще, и еще, и еще, и еще... Ну вы поняли.
В любом случае ret по итогам того же неявного преобразования будет иметь тип указателя на фукнцию:
static_assert(std::is_same_v<decltype(ret), int(*)()>);
По тем же рассуждениям, кстати, любой указатель на функцию тоже можно бесконечно разыменовать.
Спасибо, @Ivaneo, за любезно предоставленный примерчик.
Be limited only by your imagination. Stay cool.
#cppcore
❤27👍14😁11🔥2🤯1
Что нужно для запуска крутого продукта?
Качественный код, скилловая команда и дух авантюризма.
Что нужно, чтобы успешно выкатить лето на прод?
Конечно же летний ИТ‑фестиваль «Сезон кода». Там будет и код, и крутые профессионалы, и незабываемая атмосфера:
🟡 Экспертные доклады – реальные кейсы масштабирования, архитектурные боли, инструменты, которые действительно работают. От топовых бигтехов РФ. Секции:
— Клиентоориентированный код – про продукты для миллионов пользователей (win‑win, ошибки, находки).
— Продуктовая кухня (новое в 2026!) – как данные превращаются в решения, а гипотезы – в рост продукта.
— Бэкенд‑методичка – практические инструменты из повседневной инженерии.
🟡 Демозона – создатели бизнес‑платформ Т‑Банка покажут всю внутрянку: как продукты устроены и какие инженерные решения лежат в их основе.
🟡 Интерактив от Т‑Образования – если надоел литкод, а хочется нестандартных задачек.
🟡 Не одним кодом едины – будет лаунж и фотозона, спорт, афтепати с летним DJ‑сетом. И куча нетворка с крутыми инженерами.
В этом году "Сезон кода" будет проходить сразу в двух городах:
📍 Санкт‑Петербург – 20 июня, офис Т‑Банка (ИТ‑хаб ГК «Т‑Технологии»)
📍 Казань – 4 июля, ИТ‑парк им. Башира Рамеева
Переходите по ссылке, регистрируйтесь и проведите выходной за ламповыми разговорами о технологиях.
Качественный код, скилловая команда и дух авантюризма.
Что нужно, чтобы успешно выкатить лето на прод?
Конечно же летний ИТ‑фестиваль «Сезон кода». Там будет и код, и крутые профессионалы, и незабываемая атмосфера:
🟡 Экспертные доклады – реальные кейсы масштабирования, архитектурные боли, инструменты, которые действительно работают. От топовых бигтехов РФ. Секции:
— Клиентоориентированный код – про продукты для миллионов пользователей (win‑win, ошибки, находки).
— Продуктовая кухня (новое в 2026!) – как данные превращаются в решения, а гипотезы – в рост продукта.
— Бэкенд‑методичка – практические инструменты из повседневной инженерии.
🟡 Демозона – создатели бизнес‑платформ Т‑Банка покажут всю внутрянку: как продукты устроены и какие инженерные решения лежат в их основе.
🟡 Интерактив от Т‑Образования – если надоел литкод, а хочется нестандартных задачек.
🟡 Не одним кодом едины – будет лаунж и фотозона, спорт, афтепати с летним DJ‑сетом. И куча нетворка с крутыми инженерами.
В этом году "Сезон кода" будет проходить сразу в двух городах:
📍 Санкт‑Петербург – 20 июня, офис Т‑Банка (ИТ‑хаб ГК «Т‑Технологии»)
📍 Казань – 4 июля, ИТ‑парк им. Башира Рамеева
Переходите по ссылке, регистрируйтесь и проведите выходной за ламповыми разговорами о технологиях.
❤9👍5🔥5👎2
Loop unrolling. Simd
#опытным
У современных процессоров есть возможность выполнять операции над несколькими значениями одновременно. Достигается это с помощью векторных регистров - специальных длинных регистров процессора, куда могут помещаться 128 бит (SSE), 256 бит (AVX), 512 бит (AVX-512) данных. Например в 256-битном регистре помещается 4 double, 8 float, 16 short, 32 char.
После того, как мы загрузили данные в регистры, мы можем с помощью специальных инструкций(Single Instruction Multiple Data - SIMD) обрабатывать разом все значения в регистре.
Например есть такой незамысловатый код:
Здесь мы по очереди увеличиваем элементы массива.
Но их можно увеличивать сразу пачками с помощью SIMD интринсиков компилятора:
Векторные регистры работают только друг с другом, поэтому в начале помещаем число 5 в каждую 32-битную ячейку в 128-битном регистре. А потом загружаете элементы массива в другой регистр, одной операцией складываете 4 интовых значения и затем выгружаете получившиеся числа обратно в массив.
Эту ситуацию называют по-разному - и векторизация цикла и развертка цикла с использованием векторных инструкций. Количество итераций в цикле мы все равно снизили.
Вот примерчик чуть по-сложнее - полноценная функция, суммирующая 2 float массива переменной длины:
Опять же. Важно понимать, что компилятор за вас может такие простые вычисления векторизировать в том числе и с simd инструкциями. Поэтому самостоятельно используйте их только там, где компилятор бессилен.
Зато в подходящей для применения simd нетривиальной задаче вы можете получить ускорение в 2-10 раз по сравнению с ванильной версией алгоритма. Когда-то давно я работал в Intel перформанс библиотеках, так там почти все алгоритмы ускорялись интринсиками. И в отдельных случаях ускорение было на порядок(например алгоритмы блочных шифров).
Поэтому SIMD - это мощная штука. Но применять их надо применять с умом. И всегда мерять производительность кода бэнчмарками, чтобы не наоптимизировать чего лишнего.
Learn to do things simultaneously. Stay cool.
#compiler #optimization #performance
#опытным
У современных процессоров есть возможность выполнять операции над несколькими значениями одновременно. Достигается это с помощью векторных регистров - специальных длинных регистров процессора, куда могут помещаться 128 бит (SSE), 256 бит (AVX), 512 бит (AVX-512) данных. Например в 256-битном регистре помещается 4 double, 8 float, 16 short, 32 char.
После того, как мы загрузили данные в регистры, мы можем с помощью специальных инструкций(Single Instruction Multiple Data - SIMD) обрабатывать разом все значения в регистре.
Например есть такой незамысловатый код:
for (int i = 0; i < 1024; ++i) {
arr[i] += 5;
}Здесь мы по очереди увеличиваем элементы массива.
Но их можно увеличивать сразу пачками с помощью SIMD интринсиков компилятора:
__m128i five = _mm_set1_epi32(5);
for (int i = 0; i < 1024; i += 4) {
__m128i data = _mm_loadu_si128((__m128i*)&arr[i]);
data = _mm_add_epi32(data, five);
_mm_storeu_si128((__m128i*)&arr[i], data);
}
Векторные регистры работают только друг с другом, поэтому в начале помещаем число 5 в каждую 32-битную ячейку в 128-битном регистре. А потом загружаете элементы массива в другой регистр, одной операцией складываете 4 интовых значения и затем выгружаете получившиеся числа обратно в массив.
Эту ситуацию называют по-разному - и векторизация цикла и развертка цикла с использованием векторных инструкций. Количество итераций в цикле мы все равно снизили.
Вот примерчик чуть по-сложнее - полноценная функция, суммирующая 2 float массива переменной длины:
void sum_arrays_sse(const float* a, const float* b, float* c, size_t n) {
size_t i = 0;
for (; i + 3 < n; i += 4) {
// Load 4 floats from a and b
__m128 va = _mm_loadu_ps(&a[i]);
__m128 vb = _mm_loadu_ps(&b[i]);
// Add them all
__m128 vc = _mm_add_ps(va, vb);
// Store results
_mm_storeu_ps(&c[i], vc);
}
// Handle tail
for (; i < n; ++i) {
c[i] = a[i] + b[i];
}
}Опять же. Важно понимать, что компилятор за вас может такие простые вычисления векторизировать в том числе и с simd инструкциями. Поэтому самостоятельно используйте их только там, где компилятор бессилен.
Зато в подходящей для применения simd нетривиальной задаче вы можете получить ускорение в 2-10 раз по сравнению с ванильной версией алгоритма. Когда-то давно я работал в Intel перформанс библиотеках, так там почти все алгоритмы ускорялись интринсиками. И в отдельных случаях ускорение было на порядок(например алгоритмы блочных шифров).
Поэтому SIMD - это мощная штука. Но применять их надо применять с умом. И всегда мерять производительность кода бэнчмарками, чтобы не наоптимизировать чего лишнего.
Learn to do things simultaneously. Stay cool.
#compiler #optimization #performance
❤14👍8❤🔥4🔥3
Loop unrolling. Мануально
#опытным
Мы можем конечно и сами разворачивать циклы, но код будет повторяться и от этого потенциально много проблем будет.
Ну хорошо, у нас есть все инструменты, чтобы явно не повторять код. Например вот так:
Определяем парочку шаблонных функций, которые с помощью std::index_sequence и fold expression полностью изничтожают цикл и просто подряд вызывают функтор столько раз, сколько вы указали в шаблонном параметре repeat_unrolled.
Асм можете посмотреть тут. Здесь конечно есть одна хитрость с volatile, чтобы компилятор не соптимизировал наши тривиальные вычисления, но тем не менее.
Хотите частичную развертку? Да пожалуйста:
Приемы используются те же, развертку тоже можете сами увидеть.
Зачем этот пост? Да просто наткнулся на такой код при подготовке к этой серии статей. Удивился, что в нескольких источниках упоминается такой способ и решил поделиться. Не знаю на счет применимости такого кода, выглядит сомнительно и не во всех ситуациях подойдет.
Но по крайней мере где-нибудь на собеседовании можно задать такую задачку на шаблоны. И прикладной смысл понятен и не особо сложно как будто бы.
Use templates in the right place. Stay cool.
#template #interview
#опытным
Мы можем конечно и сами разворачивать циклы, но код будет повторяться и от этого потенциально много проблем будет.
Ну хорошо, у нас есть все инструменты, чтобы явно не повторять код. Например вот так:
template <typename F, std::size_t... Is>
void repeat_unrolled_impl(F&& f, std::index_sequence<Is...>)
{
((f(), void(Is)), ...);
}
template <std::size_t Iterations, typename F>
void repeat_unrolled(F&& f)
{
repeat_unrolled_impl(std::forward<F>(f),
std::make_index_sequence<Iterations>{});
}
volatile int i = 0;
int main()
{
repeat_unrolled<6>([&]{ ++i; });
}
Определяем парочку шаблонных функций, которые с помощью std::index_sequence и fold expression полностью изничтожают цикл и просто подряд вызывают функтор столько раз, сколько вы указали в шаблонном параметре repeat_unrolled.
Асм можете посмотреть тут. Здесь конечно есть одна хитрость с volatile, чтобы компилятор не соптимизировал наши тривиальные вычисления, но тем не менее.
Хотите частичную развертку? Да пожалуйста:
template <typename F, std::size_t... Offsets>
void call_with_offsets(F&& f, std::index_sequence<Offsets...>) {
((f(), void(Offsets)), ...);
}
template <std::size_t Iterations, std::size_t UnrollFactor, typename F>
void partial_unroll(F&& f) {
constexpr std::size_t full_blocks = Iterations / UnrollFactor;
constexpr std::size_t remainder = Iterations % UnrollFactor;
for (std::size_t block = 0; block < full_blocks; ++block) {
call_with_offsets(std::forward<F>(f),
std::make_index_sequence<UnrollFactor>{});
}
for (std::size_t i = 0; i < remainder; ++i) {
f();
}
}
volatile int counter = 0;
int main() {
partial_unroll<101, 4>([&]{ ++counter; });
}
Приемы используются те же, развертку тоже можете сами увидеть.
Зачем этот пост? Да просто наткнулся на такой код при подготовке к этой серии статей. Удивился, что в нескольких источниках упоминается такой способ и решил поделиться. Не знаю на счет применимости такого кода, выглядит сомнительно и не во всех ситуациях подойдет.
Но по крайней мере где-нибудь на собеседовании можно задать такую задачку на шаблоны. И прикладной смысл понятен и не особо сложно как будто бы.
Use templates in the right place. Stay cool.
#template #interview
❤15👍5🔥5🤯2
Когда ИИ-агент выходит за пределы экспериментов, одного «умного чата» становится мало. Чтобы агент был полезен в рабочей разработке, ему нужны правила, доступ к инструментам, понятный контекст, проверка действий и безопасная обвязка. Иначе вместо ускорения команда получает непредсказуемость, лишние риски и дорогой хаос в контекстном окне.
На открытом уроке 15 июня в 20:00 разберём, как устроены современные ИИ-агенты и их обвязка: правила, модули навыков и MCP — протокол подключения модели к внешним инструментам.
Поговорим, чем поведенческий слой агента отличается от слоя подключения, где искать готовые навыки, почему они стали популярны и как их устанавливать. Отдельно обсудим, как с помощью MCP дать агенту нужные инструменты, не перегружая контекст, а также как защищать агентов: схемы проверки, журналы аудита и типовые способы атак.
Урок не для тех, кто хочет просто «подключить агента к проекту» без правил, контроля и понимания рисков. И не для тех, кто считает, что рабочая интеграция ИИ — это только написать хороший запрос.
Регистрация: https://otus.pw/D8CC4/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
На открытом уроке 15 июня в 20:00 разберём, как устроены современные ИИ-агенты и их обвязка: правила, модули навыков и MCP — протокол подключения модели к внешним инструментам.
Поговорим, чем поведенческий слой агента отличается от слоя подключения, где искать готовые навыки, почему они стали популярны и как их устанавливать. Отдельно обсудим, как с помощью MCP дать агенту нужные инструменты, не перегружая контекст, а также как защищать агентов: схемы проверки, журналы аудита и типовые способы атак.
Урок не для тех, кто хочет просто «подключить агента к проекту» без правил, контроля и понимания рисков. И не для тех, кто считает, что рабочая интеграция ИИ — это только написать хороший запрос.
Регистрация: https://otus.pw/D8CC4/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
❤3👍3🔥3🥱1
Loop unrolling. Compiler
#новичкам
В прошлых постах я часто писал, что компилятор сам умеет сносно разворачивать циклы. Но это конечно оптимизация и надо знать, как ее включать. Можно конечно и прагмами, но это уже специальные подсказки в коде. Но как заставить компилятор разворачивать циклы, не трогая сам код?
Будем обсуждать все на примере gcc.
Ну и для начала: базово без оптимизаций компилятор просто генерирует код слово в слово.
В ассемблере, сгенеренном на -O0, здесь будут вызовы итераторов, честные проверки и тд
Хоть какие-то циклы компилятор начинает разворачивать на -O2. И далеко не во всех случаях. Например для сниппета выше он очень хорошо упростит код цикла, но все еще развертки не будет.
Но если спан будет фиксированного размера(да, такой есть), кратного 2-м или 4-м:
То компилятор развернет цикл c фактором 4 и дополнительно оптимизирует 4 сложения с использованием sse векторных инструкций и xmm регистров:
Однако, поменяв размер 20 на 19, то векторизация сломается.
Итого: на О2 gcc разворачивает циклы с фиксированным, кратным 2-м, числом итераций.
Тепер -O3. Здесь уже могут разворачиваться циклы с неизвестным числом итераций. Обязательно добавляется обработка хвоста и, при возможности, цикл векторизуется. Если число итераций известно, то цикл может быть полностью развернут. Вот примерчики на годболте. На этом уровне компилятор руководствуется сложными эвристиками, которые позволяют ему оценивать, будет ли в конкретном случае буст от развертки или нет.
Ну это базовые флаги оптимизации. Есть и специальные, конкретно под развертку.
В общем-то ничего нового: не нужно тупо делать вещи, которые в теории что-то делают быстрее. Профилируйте, возможно стоит просто положиться на компилятор и будет вам счастье.
Don't follow the rules blindly. Stay cool.
#compiler #optimization #performance
#новичкам
В прошлых постах я часто писал, что компилятор сам умеет сносно разворачивать циклы. Но это конечно оптимизация и надо знать, как ее включать. Можно конечно и прагмами, но это уже специальные подсказки в коде. Но как заставить компилятор разворачивать циклы, не трогая сам код?
Будем обсуждать все на примере gcc.
Ну и для начала: базово без оптимизаций компилятор просто генерирует код слово в слово.
int sum(std::span<int> ints) {
int sum = 0;
for (auto value : ints)
sum += value;
return sum;
}В ассемблере, сгенеренном на -O0, здесь будут вызовы итераторов, честные проверки и тд
Хоть какие-то циклы компилятор начинает разворачивать на -O2. И далеко не во всех случаях. Например для сниппета выше он очень хорошо упростит код цикла, но все еще развертки не будет.
Но если спан будет фиксированного размера(да, такой есть), кратного 2-м или 4-м:
int sum(std::span<int, 20> ints) {
int sum = 0;
for (auto value : ints)
sum += value;
return sum;
}То компилятор развернет цикл c фактором 4 и дополнительно оптимизирует 4 сложения с использованием sse векторных инструкций и xmm регистров:
sum(std::span<int, 20ul>):
lea rax, [rdi+80]
pxor xmm0, xmm0
.L2:
movdqu xmm2, XMMWORD PTR [rdi]
add rdi, 16
paddd xmm0, xmm2
cmp rax, rdi
jne .L2
movdqa xmm1, xmm0
psrldq xmm1, 8
paddd xmm0, xmm1
movdqa xmm1, xmm0
psrldq xmm1, 4
paddd xmm0, xmm1
movd eax, xmm0
ret
Однако, поменяв размер 20 на 19, то векторизация сломается.
Итого: на О2 gcc разворачивает циклы с фиксированным, кратным 2-м, числом итераций.
Тепер -O3. Здесь уже могут разворачиваться циклы с неизвестным числом итераций. Обязательно добавляется обработка хвоста и, при возможности, цикл векторизуется. Если число итераций известно, то цикл может быть полностью развернут. Вот примерчики на годболте. На этом уровне компилятор руководствуется сложными эвристиками, которые позволяют ему оценивать, будет ли в конкретном случае буст от развертки или нет.
Ну это базовые флаги оптимизации. Есть и специальные, конкретно под развертку.
-funroll-loops - позволяет разворачивать все циклы, количество итераций которых может быть определено во время компиляции. Если цикл маленький, он развернется полностью и цикла вообще не останется. Цитата из документации gcc: This option makes code larger, and may or may not make it run faster.-funroll-all-loops - разворачиваем все, что может быть развернуто, нас ничего не остановит. Цитата из доки: This usually makes programs run more slowly.В общем-то ничего нового: не нужно тупо делать вещи, которые в теории что-то делают быстрее. Профилируйте, возможно стоит просто положиться на компилятор и будет вам счастье.
Don't follow the rules blindly. Stay cool.
#compiler #optimization #performance
❤19👍6🔥3🤣1