#опытным
Ну хорошо. Мы поняли, что такое выравнивание, как компилятор добавляет паддинги и как узнать требования к выравниванию для типа.
Да, в основном компилятор управляет выравниваем. Но компилятор не знает всех тонкостей работы вашей программы. И не всегда корректно может выровнять данные.
В мире С++ можно контролировать почти все и alignment не исключение. Мы можем сами своими руками указать компилятору, как нужно выровнять конкретные тип.
Для этого существует C++11 спецификатор alignas. Он применяется к объявления типа или переменной и устанавливает новые требования для их выравнивания.
Сравним:
struct Vector4 {
float x, y, z, w;
};
struct alignas(16) Vector4Aligned {
float x, y, z, w;
};
std::cout << "Alignment of Vector4 is " << alignof(Vector4) << std::endl;
std::cout << "Alignment of Vector4Aligned is " << alignof(Vector4Aligned) << std::endl;
float data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
alignas(32) float aligned_data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
std::cout << "Alignment of data is " << alignof(data) << std::endl;
std::cout << "Alignment of aligned_data is " << alignof(aligned_data) << std::endl;
// OUTPUT:
// Alignment of Vector4 is 4
// Alignment of Vector4Aligned is 16
// Alignment of data is 4
// Alignment of aligned_data is 32Без alignas данные выровнены по границе 4, как это нужно типу float. Используя же спецификатор, мы можем изменить требования.
Можно также выровнять данные на основе другого типа:
struct alignas(float) struct_float
{
// ...
};
Заметьте кстати, что в предыдущих примерах размер данных не изменялся, потому что мы выровняли по границе размера данных. Но можно же использовать и более сильные требования:
struct alignas(32) Vector4AlignedTooMuch {
float x, y, z, w;
};
std::cout << "Alignment of Vector4AlignedTooMuch is "
<< alignof(Vector4AlignedTooMuch) << ", and size is "
<< sizeof(Vector4AlignedTooMuch) << std::endl;
// OUTPUT:
// Alignment of Vector4AlignedTooMuch is 32, and size is 32В этом случае размер данных увеличился в соответствие с усиленным выравниванием и добавлением паддингов.
Выравнивание переменных же аффектит только их адрес, потому что тип уже фиксирован.
Получается, что в определенных случаях мы тратим дополнительную память в угоду выравниванию. Зачем это может быть нужно?
Например, у вас есть массив мьютексов, которым пользуются разные потоки.
У каждого ядра есть свои кэш-линии для того, чтобы быстрее работать с медленной оперативной памятью. И потоки, которые получают доступ к массиву, сохраняют в кэш линию своего ядра этот массив. Получается, что потенциально в нескольких кэш-линиях будут находиться семантически одни и те же расшаренные данные.
Что будет, если одно из ядер изменит хотя бы один мьютекс? Кэш линии других ядер инвалидируются и заново будут загружаться из ОП. А это очень долго. И так будет повторяться при каждом захвате и отпускании замка.
Такая ситуация называется false sharing. Вы ожидаете, что данные не связаны и независимы, а на самом деле операции над одними данными влияют на другие.
В этом случае поможет выравнивание данных по границе размера кэш линии:
struct AlignedMutex
{
alignas(64) std::mutex mutex;
};
std::array<AlignedMutex, 10> mutexes;
Тогда каждый мьютекс будет в своей кэш-линии и операции над одним из них не будут инвалидировать соседние кэш-линии.
Другой пример использования alignas - векторные инструкции sse и avx. Современные процессоры имеют специальные инструкции для обработки нескольких данных одновременно. Этими инструкциями управляются данные в векторных регистрах, их длины - это 128, 256 или 512 бит. Для более быстрой загрузки данные должны быть выровнены по ширине соответствующих векторных регистров:
alignas(32) float aligned_data[8] = {1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f}
__m256 avx_reg = _mm256_load_ps(aligned_data);В общем, очень полезная штука, must have to know.
Have your separate space. Stay cool.
#cppcore #cpp11 #compiler