#новичкам
Представим, что мы пишем что-то связанное с графикой. У нас есть плоскость черно-белых пикселей. Каждый пиксель определяется через цвет черно-белого тона и координаты на плоскости:
struct Pixel {
unsigned char grayscale;
int x, y;
};Мы хотим сделать супер-мега-блезингово-быстрое приложение, которое при работе будет тратить всего 1 бит памяти. Ну или сколько получится, но как можно меньше. Поэтому мы паримся, как о перфомансе, так и о эффективности по памяти.
Если паримся, значит надо считать.
Посчитаем, сколько байт занимает структура Pixel. Это по сути
sizeof(unsigned char) + 2 * sizeof(int). размер char'a 1 байт, размер int на современных десктопах 4 байта. С помощью применения дифференциального исчисления в пространстве Минковского получаем 9 байт.Теория - это прекрасно, но надо все проверять:
std::cout << sizeof(Pixel) << std::endl;
И получаем..... 12 байт.
/+ 33% от расчетов. Вообще неоптимально получается. Но почему расчеты не сходятся с реальностью?
Дело вот в чём. Процессор читает данные из памяти не хаотично, а определёнными порциями, например, по 4, 8 байта за раз. Причём эти порции он читает только с определённых адресов. Например, 4-байтовое число int обычно нужно читать с адреса, который делится на 4. Чтобы понять, почему так, нужно лезть в устройство процессора и организации работы с памятью. Не будем так глубоко погружаться.
Просто представим аналогию, что процессор, как автобус, может забирать данные определенного размера только с определенных адресов, как людей с остановок.
Иногда на между остановок вообще нельзя останавливаться. Некоторые процессоры этого не позволяют. Иногда можно. Например, можно прочитать int с адреса, который на 3 делится. Но тогда нужно читать 2 соседние порции и из их кусочков составлять нужное число. А это лишние операции и накладные расходы.
То есть высокая скорость обработки данных достигается в случае, когда адреса выровнены по нужной границе.
Теперь вернемся к Pixel. Представим, что мы создаем объект структуры:
Pixel p{255, 1, 1};Исходя из рассуждений выше, логично предположить, что начало
p будет лежать по уже выравненному адресу для более быстрого чтения. Допустим, он делится на 4. В начале структуры лежит 1 байт цвета. А после лежат 2 int'а по 4 байта, доступ к которым должен быть выровнен. Но если к числу, кратному 4-м, прибавить 1, то оно перестанет делиться на 4. То есть, если данные лежат прям последовательно, то доступ будет невыровненный, а значит медленный.Но компилятор у нас - не тупая машина, питающаяся текстом программы. Он умный и умеет оптимизировать. Если для скорости работы программы нужно, что адреса
x и y должны быть выровнены по четверке, то он сделает магию и выровняет их. Заклинание простое - добавляем 3 байта после поля grayscale и дело в шляпе! Вот поэтому на практике размер Pixel равен 12 байтам.Ну ладно, мы еще повоюем за память. Попробуем перетасовать поля:
struct Pixel {
int x, y;
unsigned char grayscale;
};Теперь-то будет 9 байт размер?!
Да нет. Все те же 12 байт.
Представьте, что вы сделали массив пикселей. Да, у первого элемента
x и y выровнены по 4-ке. Но начиная со второго элемента выравнивание поедет к черту. Так не годится и приходят на помощь все те же 3 байта, только уже в конце структуры.Кстати, эти дополнительные куски памяти, которые компилятор добавляет для обеспечения выравнивания, называются паддингами(padding). В них не хранятся данные, они нужны чисто для выравнивания.
Сегодня мы рассмотрели, фактически, только предназначение выравнивания. В следующих постах будем раскрывать подробности.
Align yourself. Stay cool.
#cppcore #optimization #compiler