Forwarded from Блог*
#prog #cpp #моё
В C++ есть такая вещь, как strict aliasing. Если коротко, то это предположение компилятора о том, что доступы по указателям (и ссылкам) существенно разных типов не пересекаются между собой. Подробнее про это можно прочитать, например, тут, ну а я покажу, как это влияет на (не)возможность оптимизировать код на C++. Все приведённые ниже примеры будут использовать компилятор GCC 12.2 с флагами
Напишем вот такой код (где
Смысл этого кода очень простой: увеличить все числа в данном диапазоне на данную величину. Казалось бы, тут в цикле есть доступ по указателю, который имеет смысл вынести из тела (loop-invariant code motion). Но во что этот код переводит компилятор?
Дело в том, что переданный указатель может указывать на сам элемент внутри спана.
Замена указателя на ссылку ожидаемо не даёт никаких изменений. Вынос разыменовывания значения для инкремента в отдельную переменную перед циклом и использование её вместо указателя даёт желаемый результат в кодгене. Но что, если мы будем передавать указатели на другие типы?
Попробуем, например,
Вот тут как раз и вступает в силу правила strict aliasing (aka последний параграф в [basic.lval]): не смотря на то, что сформировать указатель на
Однако! У правил strict aliasing есть нюансы насчёт того, по указателям (на самом деле glvalue, но не суть) каких типов можно получать доступ к значениям других типов. В частности,
В C++ есть такая вещь, как strict aliasing. Если коротко, то это предположение компилятора о том, что доступы по указателям (и ссылкам) существенно разных типов не пересекаются между собой. Подробнее про это можно прочитать, например, тут, ну а я покажу, как это влияет на (не)возможность оптимизировать код на C++. Все приведённые ниже примеры будут использовать компилятор GCC 12.2 с флагами
--std=c++20 -O2 -pedantic (с -O3 компилятор векторизует код и делает его гораздо объёмнее и менее понятным).Напишем вот такой код (где
std::span<int> играет примерно ту же роль, что и &mut [i32] в Rust):#include <span>
void increment(std::span<int> arr, const int* value) {
for (auto& x: arr) {
x += *value;
}
}
(в дальнейшем для экономии места я буду опускать #include <span>)Смысл этого кода очень простой: увеличить все числа в данном диапазоне на данную величину. Казалось бы, тут в цикле есть доступ по указателю, который имеет смысл вынести из тела (loop-invariant code motion). Но во что этот код переводит компилятор?
lea rcx, [rdi+rsi*4]
cmp rdi, rcx
je .L1
.L3:
mov eax, DWORD PTR [rdx]
add DWORD PTR [rdi], eax
add rdi, 4
cmp rdi, rcx
jne .L3
.L1:
ret
Тело цикла располагается между метками .L1 и .L3. Конкретно сейчас нас интересуют две инструкции:mov eax, DWORD PTR [rdx]
add DWORD PTR [rdi], eax
Выходит, что в регистре rdx располагается адрес, указатель value, а в регистре rdi — x, адрес текущего элемента спана. На каждой итерации процессор загружает значение из памяти и потом складывает со значением в другом месте в памяти. Почему же?Дело в том, что переданный указатель может указывать на сам элемент внутри спана.
increment может быть использована, например, так:void use_increment(std::span<int> a) {
increment(a, &a[1]);
}
И const в составе типа указателя тут не панацея: он лишь запрещает модифицировать доступ через указатель, но не гарантирует, что значение, на которое он указывает, действительно не изменяется. Выходит, компилятор генерирует неэффективный код ради того, чтобы правильно работал случай, который программист навряд ли стал бы писать намеренно.Замена указателя на ссылку ожидаемо не даёт никаких изменений. Вынос разыменовывания значения для инкремента в отдельную переменную перед циклом и использование её вместо указателя даёт желаемый результат в кодгене. Но что, если мы будем передавать указатели на другие типы?
Попробуем, например,
short:void increment(std::span<int> arr, const short* value) {
// тело без изменений
}
Что генерирует компилятор? lea rax, [rdi+rsi*4]
cmp rdi, rax
je .L1
movsx edx, WORD PTR [rdx]
.L3:
add DWORD PTR [rdi], edx
add rdi, 4
cmp rdi, rax
jne .L3
.L1:
ret
Ага, то есть доступ к value (с sign extension, разумеется) —movsx edx, WORD PTR [rdx]
— вынесен за пределы цикла! Так в чём же разница по сравнению с предыдущими примерами?Вот тут как раз и вступает в силу правила strict aliasing (aka последний параграф в [basic.lval]): не смотря на то, что сформировать указатель на
short из указателя на int можно, эти два типа отличаются, и получение доступа к значению одного типа через указатель на другой является неопределённым поведением. Так как в корректной программе на C++ неопределённого поведения не может произойти, компилятор использует этот факт, чтобы обосновать корректность выноса доступа к памяти из цикла.Однако! У правил strict aliasing есть нюансы насчёт того, по указателям (на самом деле glvalue, но не суть) каких типов можно получать доступ к значениям других типов. В частности,
unsigned и signed варианты того же типа не считаются существенно отличными, и потому при передаче const unsigned* value компилятор оставляет доступ к value в теле цикла.Gist
What is Strict Aliasing and Why do we Care?
What is Strict Aliasing and Why do we Care? GitHub Gist: instantly share code, notes, and snippets.
🔥3
Про RVO и NRVO в C++
Начало: https://xn--r1a.website/thisnotes/198
Продолжение: https://xn--r1a.website/thisnotes/200
Начало: https://xn--r1a.website/thisnotes/198
Продолжение: https://xn--r1a.website/thisnotes/200
Telegram
this->notes.
#cpp
RVO/NRVO 1/2.
Давно хотел рассказать про всякие оптимизации, которые делаются в плюсах. Тут будет пост про RVO/NRVO, а позже про что-нибудь ещё.
Может в начале будут какие-то неявные допущения или грубые формулировки, но к концу поста постараюсь…
RVO/NRVO 1/2.
Давно хотел рассказать про всякие оптимизации, которые делаются в плюсах. Тут будет пост про RVO/NRVO, а позже про что-нибудь ещё.
Может в начале будут какие-то неявные допущения или грубые формулировки, но к концу поста постараюсь…
🐳3