Telegram Group & Telegram Channel
#prog #cpp #моё

В 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, а в регистре rdix, адрес текущего элемента спана. На каждой итерации процессор загружает значение из памяти и потом складывает со значением в другом месте в памяти. Почему же?

Дело в том, что переданный указатель может указывать на сам элемент внутри спана. 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 в теле цикла.



group-telegram.com/dereference_pointer_there/4196
Create:
Last Update:

#prog #cpp #моё

В 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, а в регистре rdix, адрес текущего элемента спана. На каждой итерации процессор загружает значение из памяти и потом складывает со значением в другом месте в памяти. Почему же?

Дело в том, что переданный указатель может указывать на сам элемент внутри спана. 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 в теле цикла.

BY Блог*




Share with your friend now:
group-telegram.com/dereference_pointer_there/4196

View MORE
Open in Telegram


Telegram | DID YOU KNOW?

Date: |

Two days after Russia invaded Ukraine, an account on the Telegram messaging platform posing as President Volodymyr Zelenskiy urged his armed forces to surrender. One thing that Telegram now offers to all users is the ability to “disappear” messages or set remote deletion deadlines. That enables users to have much more control over how long people can access what you’re sending them. Given that Russian law enforcement officials are reportedly (via Insider) stopping people in the street and demanding to read their text messages, this could be vital to protect individuals from reprisals. The Security Service of Ukraine said in a tweet that it was able to effectively target Russian convoys near Kyiv because of messages sent to an official Telegram bot account called "STOP Russian War." On February 27th, Durov posted that Channels were becoming a source of unverified information and that the company lacks the ability to check on their veracity. He urged users to be mistrustful of the things shared on Channels, and initially threatened to block the feature in the countries involved for the length of the war, saying that he didn’t want Telegram to be used to aggravate conflict or incite ethnic hatred. He did, however, walk back this plan when it became clear that they had also become a vital communications tool for Ukrainian officials and citizens to help coordinate their resistance and evacuations. Recently, Durav wrote on his Telegram channel that users' right to privacy, in light of the war in Ukraine, is "sacred, now more than ever."
from us


Telegram Блог*
FROM American