Как организовать элементы в структуре, чтобы тратить минимум места на выравнивание?

[Не копия заполнения и упаковки структуры. Этот вопрос о том, как и когда происходит заполнение. Это о том, как с этим бороться.]

Я только что понял, сколько памяти тратится впустую в результате выравнивания в C++. Рассмотрим следующий простой пример:

struct X
{
    int a;
    double b;
    int c;
};

int main()
{
    cout << "sizeof(int) = "                      << sizeof(int)                      << '\n';
    cout << "sizeof(double) = "                   << sizeof(double)                   << '\n';
    cout << "2 * sizeof(int) + sizeof(double) = " << 2 * sizeof(int) + sizeof(double) << '\n';
    cout << "but sizeof(X) = "                    << sizeof(X)                        << '\n';
}

При использовании g++ программа выдает следующий результат:

sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 24

Это 50% накладных расходов памяти! В 3-гигабайтном массиве из 134 217 728 Xs 1 гигабайт будет чистым дополнением.

К счастью, решение проблемы очень простое — нам просто нужно поменять местами double b и int c:

struct X
{
    int a;
    int c;
    double b;
};

Теперь результат гораздо более удовлетворительный:

sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 16

Однако есть проблема: это несовместимо. Да, в g++ int составляет 4 байта, а double — 8 байт, но это не всегда верно (их выравнивание также не обязательно должно быть одинаковым), поэтому в другой среде это исправление может быть не только бесполезным, но и это также может потенциально ухудшить ситуацию, увеличив количество необходимого заполнения.

Есть ли надежный межплатформенный способ решения этой проблемы (минимизировать количество необходимых отступов без снижения производительности из-за смещения)? Почему компилятор не выполняет такую ​​оптимизацию (меняет местами элементы структуры/класса, чтобы уменьшить отступы)?

Уточнение

Из-за недопонимания и путаницы я хотел бы подчеркнуть, что я не хочу упаковывать свои struct. То есть я не хочу, чтобы его члены были невыровненными и, следовательно, доступ к ним был бы медленнее. Вместо этого я по-прежнему хочу, чтобы все члены были самовыравнивающимися, но таким образом, чтобы использовать наименьшее количество памяти для заполнения. Эту проблему можно решить, например, с помощью ручной перестановки, как описано здесь и в Утерянное искусство упаковки. Эрик Рэймонд. Я ищу автоматизированный и максимально кроссплатформенный способ сделать это, подобный тому, что описан в предложение P1112 для будущего стандарта C++20.


person Community    schedule 25.06.2019    source источник
comment
Если вам нужны массивы из сотен миллионов элементов, то, возможно, массивы не являются правильной структурой данных для начала? По крайней мере, не массивы в памяти (например, файлы с отображением памяти или, возможно, даже какая-то база данных)?   -  person Some programmer dude    schedule 25.06.2019
comment
И действительно, единственный возможный ответ на вопрос: существует ли надежный кросс-платформенный способ решить эту проблему (минимизировать количество необходимых отступов, не страдая от снижения производительности из-за смещения)? может быть только простое нет. Вероятно, существуют специфичные для компилятора и системы способы обойти это, но ничего действительно переносимого или независимого от компилятора/платформы/системы.   -  person Some programmer dude    schedule 25.06.2019
comment
Использование целых чисел фиксированной ширины может дать некоторые преимущества переносимости, чтобы они не менялись. размер на тебе.   -  person user4581301    schedule 25.06.2019
comment
А что касается [почему] почему компилятор не выполняет такие оптимизации (меняет местами элементы структуры/класса, чтобы уменьшить заполнение)? Как компилятор может это сделать, если он не может сказать, для чего используется структура? Возможно, он будет сохранен в необработанном виде в двоичном файле или отправлен по протоколу последовательной связи (в этом случае распакованные структуры (вручную или с помощью прагмы компилятора) действительно плохая идея, но все же это происходит).   -  person Some programmer dude    schedule 25.06.2019
comment
самые большие требования к выравниванию first. Если нет, то самые крупные элементы сначала. Что касается вашего реального вопроса, да, для этого существует кросс-совместимый метод: он называется string. Помимо этого, типы, использующие заданную разрядность, могут значительно помочь, но все равно требуют обработки порядка байтов, если вы действительно серьезно относитесь к кроссплатформенности. Короче говоря, протоколы существуют специально для решения таких проблем и преодоления жестких различий между платформами. Подобные вещи являются одной из многих причин, по которым они существуют. Предупреждение: хороший шанс, что я совершенно неправильно понял суть этого вопроса.   -  person WhozCraig    schedule 25.06.2019
comment
Наконец, для меня это очень похоже на XY-проблему. Перестановка структур — это решение, но в чем настоящая проблема, стоящая за этим решением? Чего вы на самом деле пытаетесь достичь? Зачем вам массивы из миллионов структур? Возможно, есть другие возможные решения этой первоначальной проблемы, которые не включают массивы или делают возможное заполнение неуместным?   -  person Some programmer dude    schedule 25.06.2019
comment
По всем вышеперечисленным причинам не существует ничего, что гарантировало бы минимальный размер хранилища для структуры, но @WhozCraig дает точное объяснение чрезмерно упрощенного правила Сначала самый большой, последний с наименьшим в порядке убывания требуемого размера хранилища. . Это примерно такой же разумный подход, который может минимизировать объем памяти между компиляторами и оборудованием, но нет гарантии, что любые две структуры будут выделены одинаковым объемом памяти между компиляторами (кроме тривиальных примеров (таких как struct foo { int a, b; };)   -  person David C. Rankin    schedule 26.06.2019
comment
@Someprogrammerdude Зачем вам нужны массивы из миллионов структур? Думаю, в HPC это довольно распространено. Например, мы работаем с очень большими разреженными матрицами. Наш типичный рабочий процесс заключается в создании матричных элементов, а затем в преобразовании их в формат хранения, эффективный для дальнейшей обработки. Это преобразование обычно включает сортировку. К сожалению, C++ не поддерживает сортировку сразу нескольких массивов, поэтому мы сортируем их в виде массива структур, каждая из которых имеет индекс строки/столбца и значение. Мы можем работать даже с миллиардами матричных элементов в одном процессе MPI.   -  person Daniel Langr    schedule 26.06.2019
comment
Ваше описание отказа от упаковки структуры звучит точно так же, как упаковка структуры.   -  person chrylis -cautiouslyoptimistic-    schedule 26.06.2019
comment
в g++ int занимает 4 байта, а double — 8 байт. Что ж, на Arduino (базовый компилятор — GCC, используемый как компилятор C++) double совпадает с float (4 байта), что может стать неожиданностью для некоторых (особенно если более 7-8 значащих цифр). требуются, скажем, для частотомера...).   -  person Peter Mortensen    schedule 26.06.2019
comment
Возможный дубликат заполнения и упаковки структуры   -  person John Bollinger    schedule 26.06.2019
comment
@chrylis не означает, что упаковка структуры влечет за собой невыровненный доступ? Есть средний способ, когда вы переупорядочиваете элементы.   -  person RonJohn    schedule 27.06.2019
comment
@ РонДжон Не обязательно. В частности, для выравнивания обычно используется что-то вроде большего размера слова или операнда, что означает, что (int, int, double) естественным образом выравнивается без заполнения.   -  person chrylis -cautiouslyoptimistic-    schedule 27.06.2019
comment
Если вы нашли этот вопрос полезным, тогда здесь приведены некоторые другие способы оптимизации кода на низком уровне.   -  person    schedule 27.06.2019
comment
@DanielLangr Я спросил, потому что хотел, чтобы ОП подробно рассказал о реальной проблеме, а не о том, как исправить решение неизвестной (для нас) проблемы.   -  person Some programmer dude    schedule 01.07.2019


Ответы (7)


(Не применяйте эти правила без раздумий. См. пункт ESR о локальности кеша для членов, которые вы используете вместе. И в многопоточных программах остерегайтесь ложного совместного использования элементов, написанных разными потоками. Как правило, вам не нужны данные для каждого потока в по этой причине вообще используйте одну структуру, если только вы не делаете это для управления разделением с помощью большого alignas(128). Это относится к atomic и неатомарным переменным; важно то, что потоки пишут в строки кеша независимо от того, как они это делают.)


Правило большого пальца: от большего к меньшему alignof(). Вы не можете сделать ничего идеального везде, но, безусловно, наиболее распространенным случаем в наши дни является разумная нормальная реализация C++ для обычного 32- или 64-битного процессора. Все примитивные типы имеют размеры степени двойки.

Большинство типов имеют alignof(T) = sizeof(T) или alignof(T), ограниченные шириной регистра реализации. Таким образом, более крупные типы обычно более выровнены, чем более мелкие.

Правила упаковки структур в большинстве ABI дают членам структуры их абсолютное alignof(T) выравнивание относительно начала структуры, а сама структура наследует наибольшее alignof() из всех своих членов.

  • Всегда ставьте 64-разрядные элементы на первое место (например, double, long long и int64_t). ISO C++, конечно, не исправляет эти типы в 64 бита / 8 байт, но на практике на всех процессорах, которые вам нужны, они есть. Люди, переносящие ваш код на экзотические процессоры, могут при необходимости оптимизировать макеты структур.

  • затем указатели и целые числа ширины указателя: size_t, intptr_t и ptrdiff_t (которые могут быть 32- или 64-разрядными). Все они имеют одинаковую ширину в обычных современных реализациях C++ для процессоров с плоской моделью памяти.

    Если вы заботитесь о процессорах x86 и Intel, рассмотрите возможность размещения указателей связанного списка и дерева влево/вправо в первую очередь. Поиск указателя по узлам в дереве или связанном списке than-the-base">налагает штрафы, когда начальный адрес структуры находится на странице размером 4 КБ, отличной от той, к которой вы обращаетесь. Ставя их на первое место, вы гарантируете, что этого не может быть.

  • затем long (который иногда бывает 32-битным, даже когда указатели 64-битные, в LLP64 ABI, таких как Windows x64). Но гарантированно ширина не меньше int.

  • затем 32-разрядные int32_t, int, float, enum. (При желании разделите int32_t и float перед int, если вас интересуют возможные 8/16-битные системы, которые по-прежнему дополняют эти типы до 32-битных или лучше работают с их естественным выравниванием. Большинство таких систем не имеют более широких нагрузок (FPU или SIMD), поэтому более широкие типы все равно должны обрабатываться как несколько отдельных фрагментов).

    ISO C++ допускает, что int может быть как 16-битным, так и произвольно широким, но на практике это 32-битный тип даже на 64-битных процессорах. Разработчики ABI обнаружили, что программы, предназначенные для работы с 32-битными int, просто тратят память (и объем кэш-памяти), если int шире. Не делайте предположений, которые могут привести к проблемам с корректностью, но для портативной производительности вы просто должны быть правы в обычном случае.

    Люди, настраивающие ваш код для экзотических платформ, могут при необходимости подправить его. Если определенный макет структуры критичен для производительности, возможно, прокомментируйте свои предположения и рассуждения в заголовке.

  • затем short / int16_t

  • затем char / int8_t / bool

  • (для нескольких флагов bool, особенно если они в основном предназначены для чтения или если они все изменяются вместе, рассмотрите возможность упаковки их с 1-битными битовыми полями.)

(Для целочисленных типов без знака найдите соответствующий тип со знаком в моем списке.)

массив кратный 8 байтам более узких типов может идти раньше, если вы этого хотите. Но если вы не знаете точных размеров типов, вы не можете гарантировать, что int i + char buf[4] заполнит 8-байтовый выровненный слот между двумя double. Но это неплохое предположение, поэтому я бы все равно сделал это, если бы была какая-то причина (например, пространственная локальность элементов, к которым осуществляется доступ вместе) для их объединения, а не в конце.

Экзотические типы: x86-64 System V имеет alignof(long double) = 16, но i386 System V имеет только alignof(long double) = 4, sizeof(long double) = 12. Это 80-битный тип x87, который на самом деле составляет 10 байт, но дополнен до 12 или 16, поэтому он кратен его alignof, что делает возможным создание массивов без нарушения гарантии выравнивания.

И в целом это становится сложнее, когда члены вашей структуры сами являются агрегатами (структурой или объединением) с sizeof(x) != alignof(x).

Еще одна особенность заключается в том, что в некоторых ABI (например, в 32-разрядной Windows, если я правильно помню) элементы структуры выравниваются по их размеру (до 8 байтов) относительно начала структуры, хотя alignof(T) по-прежнему только 4 для double и int64_t.
Это сделано для оптимизации общего случая раздельного выделения 8-байтовой выровненной памяти для одной структуры без предоставления гарантии выравнивания. i386 System V также имеет тот же alignof(T) = 4 для большинства примитивных типов (но malloc по-прежнему дает вам 8-байтовую выровненную память, потому что alignof(maxalign_t) = 8). Но в любом случае, i386 System V не имеет этого правила упаковки структур, поэтому (если вы не упорядочиваете свою структуру от наибольшего к наименьшему) вы можете получить 8-байтовые элементы, выровненные ниже относительно начала структуры. .


Большинство ЦП имеют режимы адресации, которые при наличии указателя в регистре позволяют получить доступ к любому смещению байта. Максимальное смещение обычно очень велико, но на x86 оно экономит размер кода, если смещение в байтах соответствует байту со знаком ([-128 .. +127]). Поэтому, если у вас есть большой массив любого типа, лучше поместить его в структуру позже после часто используемых элементов. Даже если это стоит немного набивки.

Ваш компилятор почти всегда будет создавать код с адресом структуры в регистре, а не с каким-то адресом в середине структуры, чтобы воспользоваться преимуществами коротких отрицательных смещений.


Эрик С. Рэймонд написал статью Утерянное искусство упаковки структур. В частности, ответом на этот вопрос является раздел переупорядочения структуры.

Он также отмечает еще один важный момент:

9. Удобочитаемость и локальность кэша

Хотя переупорядочивание по размеру — самый простой способ избавиться от небрежности, это не всегда правильно. Есть еще две проблемы: читабельность и локальность кеша.

В большой структуре, которую можно легко разделить по границе строки кеша, имеет смысл разместить 2 элемента рядом, если они всегда используются вместе. Или даже смежные, чтобы разрешить объединение загрузки/сохранения, например. копирование 8 или 16 байтов с одним (не выровненным) целым числом или загрузкой/сохранением SIMD вместо отдельной загрузки меньших элементов.

Строки кэша обычно имеют размер 32 или 64 байта на современных процессорах. (На современном x86 всегда 64 байта. А семейство Sandybridge имеет пространственную предварительную выборку смежных строк в кэше L2, которая пытается завершить 128-байтовые пары строк, отдельно от детектора шаблонов предварительной выборки HW основного стримера L2 и предварительной выборки L1d).


Забавный факт: Rust позволяет компилятору переупорядочивать структуры для лучшей упаковки или по другим причинам. IDK, если какие-либо компиляторы действительно это делают. Вероятно, это возможно только при оптимизации всей программы во время компоновки, если вы хотите, чтобы выбор основывался на том, как фактически используется структура. В противном случае раздельно составленные части программы не могли согласоваться по компоновке.


(@alexis опубликовал ответ только со ссылкой на статью ESR, так что спасибо за эту отправную точку.)

person Peter Cordes    schedule 26.06.2019
comment
Хотя это на самом деле не полностью кроссплатформенное решение и не автоматизированное, оно содержит самую актуальную информацию о том, как можно решить эту проблему, так что я приму его. Возможно, позже я создам здесь вики-сообщество. - person ; 26.06.2019
comment
@YanB.: Я не полностью прочитал вопрос, прежде чем ответить; Я не знал, что вы в основном ищете автоматизированные решения, а не эмпирические правила. Но, к счастью, между всеми современными массовыми 32- и 64-битными процессорами достаточно сходства, о котором мы действительно заботимся, чтобы мы могли дать полезный совет, несмотря на то, что ISO C++ практически ничего не гарантирует. Существует большой набор предположений о том, что нормально для C++ (и современных процессоров), отдельно от стандарта ISO C++. Многое из этого почти необходимо для того, чтобы реализация C++ была полезна для чего-либо на практике! - person Peter Cordes; 26.06.2019
comment
Порядок сортировки от меньшего к большему, возможно, в целом лучше: он приводит к более эффективному доступу к большинству членов (например, из-за того, что смещение меньше, как вы указываете, а также из-за того, что больше членов структуры имеют тенденцию попадать в строку кэша). Основным недостатком является то, что дополнительные отверстия чаще появляются в середине структуры, а не в конце, поэтому в некоторых необычных случаях копирование может быть менее эффективным. - person BeeOnRope; 27.06.2019
comment
@BeeOnRope: особенно с пропущенными оптимизациями gcc. Объединение хранилища GCC8 для обнуления структур отказывается перезаписывать заполнение: gcc.gnu.org/bugzilla /show_bug.cgi?id=82142 - person Peter Cordes; 27.06.2019
comment
Это не кажется универсальной проблемой. См. мой быстрый тест. - person BeeOnRope; 27.06.2019

gcc имеет предупреждение -Wpadded, которое предупреждает, когда к структуре добавляется отступ:

https://godbolt.org/z/iwO5Q3:

<source>:4:12: warning: padding struct to align 'X::b' [-Wpadded]
    4 |     double b;
      |            ^

<source>:1:8: warning: padding struct size to alignment boundary [-Wpadded]
    1 | struct X
      |        ^

И вы можете вручную переставить элементы, чтобы было меньше/нет отступов. Но это не кросс-платформенное решение, так как разные типы могут иметь разные размеры/выравнивания в разных системах (в первую очередь указатели имеют размер 4 или 8 байт на разных архитектурах). Общее эмпирическое правило заключается в том, чтобы при объявлении членов переходить от наибольшего к наименьшему выравниванию, и если вы все еще беспокоитесь, скомпилируйте свой код с помощью -Wpadded один раз (но я бы не стал оставлять его в целом, потому что иногда необходимо заполнение).

Что касается причины, по которой компилятор не может сделать это автоматически, то из-за стандарта ([ class.mem]/19). Это гарантирует, что, поскольку это простая структура, состоящая только из открытых членов, &x.a < &x.c (для некоторых X x;), их нельзя переупорядочить.

person Artyer    schedule 25.06.2019
comment
Честно говоря, я не думал, что увижу что-то полезное из этого вопроса. Не знал об этой опции gcc (и теперь я надеюсь, что она есть и у clang). Спасибо, что научил меня кое-чему. галочка. - person WhozCraig; 25.06.2019
comment
@WhozCraig Да, у clang тоже есть эта опция (у нее даже такое же имя). Это очень полезно (по крайней мере, для меня) при решении проблемы перестановки. Обидно, что (по крайней мере, на данный момент) я не нашел автоматизированного решения. - person ; 26.06.2019
comment
Существуют ли какие-либо отдаленно современные платформы, в которых размещение типов в порядке double, [unsigned] long long, [i]int64_t, int64_t, указатели, long, float, int32_t, int, int16_t, short, char не привело бы к оптимальному выравниванию? - person supercat; 29.06.2019

В общем случае переносного решения действительно нет. При минимальных требованиях, предъявляемых стандартом, типы могут быть любого размера, который захочет реализовать.

Чтобы согласиться с этим, компилятору не разрешено переупорядочивать члены класса, чтобы сделать его более эффективным. Стандарт предписывает, что объекты должны располагаться в объявленном порядке (по модификатору доступа), так что это тоже исключено.

Вы можете использовать типы с фиксированной шириной, например

struct foo
{
    int64_t a;
    int16_t b;
    int8_t c;
    int8_t d;
};

и это будет одинаково на всех платформах, если они предоставляют эти типы, но это работает только с целочисленными типами. Не существует типов с плавающей запятой фиксированной ширины, и многие стандартные объекты/контейнеры могут иметь разные размеры на разных платформах.

person NathanOliver    schedule 25.06.2019
comment
Добавляя соль на рану, типы с плавающей запятой часто гиперчувствительны к положениям выравнивания шины, тем самым усиливая мантру «нет серебряной пули». Несмотря на это, это очень полезно при загрузке структур с чем-либо, кроме плавающей запятой и, возможно, указателей. Я использую его часто. - person WhozCraig; 25.06.2019
comment
Почему не допускается перестановка элементов? Не могли бы вы уточнить? - person ; 26.06.2019
comment
Если вы доводите кросс-платформенную переносимость до предела, обратите внимание, что именно эти типы ширины являются необязательными. Каждая платформа должна иметь int_least16_t и int_fast16_t, но (например, если CHAR_BIT != 8), int16_t не обязательно должна существовать на данной платформе. - person DevSolar; 26.06.2019
comment
@DevSolar Хотя они являются необязательными, код не скомпилируется, если их нет, поэтому, по крайней мере, вы не получите двоичный файл, который взорвется на вас. - person NathanOliver; 26.06.2019
comment
Вы можете хранить число с плавающей запятой в 4-байтовом int. Только читать и писать отвратительно. - person Oblivion; 26.06.2019
comment
@ЯнБ. Потому что так написано в стандарте. Также см. stackoverflow.com/questions/118068/. Что касается обоснования, то многое было бы нарушено, если бы компиляторы могли это делать (среди прочего, представьте себе программу, которая записывает structs непосредственно в файлы с fwrite и считывает их обратно с fread; изменения в компиляторе могут внезапно нарушить формат файла совместимость для скомпилированных программ). - person jamesdlin; 26.06.2019

Это проблема учебника памяти против скорости. Заполнение заключается в обмене памяти на скорость. Вы не можете сказать:

Я не хочу «упаковывать» свою структуру.

потому что прагма-пакет — это инструмент, изобретенный именно для того, чтобы сделать эту сделку по-другому: скорость для памяти.

Есть ли надежный кроссплатформенный способ

Нет, не может быть. Выравнивание строго зависит от платформы. Размер различных типов зависит от платформы. Избегание заполнения путем реорганизации зависит от платформы в квадрате.

Скорость, память и кроссплатформенность — у вас может быть только два.

Почему компилятор не выполняет такую ​​оптимизацию (меняет местами элементы структуры/класса, чтобы уменьшить отступы)?

Потому что спецификация C++ гарантирует, что компилятор не испортит ваши тщательно организованные структуры. Представьте, что у вас есть четыре поплавка подряд. Иногда вы используете их по имени, а иногда вы передаете их методу, который принимает параметр float[3].

Вы предлагаете, чтобы компилятор перетасовал их, потенциально сломав весь код с 1970-х годов. И по какой причине? Можете ли вы гарантировать, что каждый программист действительно захочет сохранить ваши 8 байтов на структуру? Я, например, уверен, что если у меня массив 3 ГБ, у меня проблемы посерьезнее, чем с гигабайтом больше или меньше.

person Agent_L    schedule 26.06.2019
comment
Я бы сказал, что единственная проблема здесь заключается в том, что «иногда вы передаете их методу, который принимает float[3] параметр». Ну, это довольно особый вариант использования. На самом деле я бы сказал, что главная проблема заключается в том, что C++ поддерживает такое жонглирование указателями; если бы он этого не делал и вместо этого позволял компилятору всегда переупорядочивать для оптимизации, тогда большая часть кода работала бы быстрее, в то время как программы, которые нужно было бы переписать, чтобы явно обернуть этот float[3] в массив, имели бы незначительное снижение производительности. - person leftaroundabout; 26.06.2019
comment
Я почти уверен, что ввод четырех отдельных переменных-членов с плавающей запятой для передачи их как float[3] вызывает неопределенное поведение. - person Jeremy Friesner; 26.06.2019
comment
@JeremyFriesner: Обратите внимание, что Undefined Behavior был предназначен для того, чтобы позволить реализациям, которые могли бы предложить более полезную семантику, делать это, когда это было целесообразно, до того, как языковые вандалы взяли верх и начали использовать его в качестве предлога, чтобы не предлагать полезную семантику даже в случаях, когда они ничего не стоили . - person supercat; 26.06.2019
comment
@supercat, независимо от исторических намерений, вызов неопределенного поведения - это не то, что хочется делать (если только вам не нравится обнаруживать и диагностировать неясные нарушения поведения во время выполнения) - person Jeremy Friesner; 26.06.2019
comment
@JeremyFriesner: Стандарт никогда не требовал, чтобы реализации поддерживали всю семантику, необходимую для какой-либо конкретной цели. На многих целевых платформах ввод-вывод был бы невозможен без использования указателей для представления адресов, которые не идентифицируют объекты, как это определено в Стандарте. Если бы не разрешалось совершать действия, к которым Стандарт не предъявляет требований, то на таких платформах нельзя было бы ничего делать. - person supercat; 26.06.2019
comment
@JeremyFriesner: Конечно, можно было бы напроситься на неприятности, если бы кто-то попытался использовать такие методы низкоуровневого программирования в реализации, которая не была разработана или настроена для таких целей, но с использованием реализация, которая не подходит для какой-либо конкретной работы, которую вы пытаетесь выполнить, вызовет проблемы. - person supercat; 26.06.2019
comment
@supercat на самом деле это были не языковые вандалы, а авторы компиляторов, которые смогли выжать еще несколько возможностей оптимизации, буквально восприняв неопределенное поведение. По сути, вы хотите, чтобы компилятор сделал что-то разумное, в то время как автор компилятора предпочитает делать что-то быстрое (потому что это улучшает тесты, которые, в свою очередь, улучшают продажи/распространение информации и фактически улучшают скорость выполнения даже для вполне обычных программ). - person toolforger; 26.06.2019
comment
@toolforger: Вы читали опубликованное Обоснование? По мнению Комитета, наиболее фундаментальными аспектами духа C являются доверие к программисту и не мешай программисту делать то, что нужно. Они также явно признают, что одной из сильных сторон языка C является возможность использовать непереносимые программы для выполнения задач, которые переносимые программы делать не могут (поскольку Стандарт не предусматривает их). Если какая-то задача не может быть выполнена без выполнения какого-либо действия, все реализации, подходящие для этой задачи, будут поддерживать это действие независимо от того, требуется ли это стандартом. - person supercat; 26.06.2019
comment
@toolforger: авторы компиляторов вводят ложную дихотомию между скоростью и семантикой. Если компилятор иногда обрабатывает целые числа со знаком так, как если бы они выполнялись для более широкого типа, это позволило бы включить 90%+ полезных оптимизаций, связанных с переходом на рельсы при переполнении. Если бы такому компилятору был предоставлен исходный код, который использует тот факт, что это все, он мог бы добиться оптимизации, которая была бы невозможна с исходным кодом, написанным для модели переполнения, которую следует избегать любой ценой. - person supercat; 26.06.2019
comment
@toolforger: В более общем смысле оптимизация, предполагающая, что программисту не нужно будет делать X, может быть полезна для программ, которым нужно делать X, но будет контрпродуктивной в тех случаях, когда требуемое поведение — это именно то, что было бы достигнуто с помощью просто выполнение X. Если действие X требуется для одних задач, но не для других, и если затраты на поддержку X в разных реализациях различаются, X следует поддерживать в реализациях или конфигурациях, используемых для задач, требующих этого, но не в тех, где это было бы невозможно. навязывать ненужные расходы. Это должно быть само собой разумеющимся, но, видимо, это не так. - person supercat; 26.06.2019
comment
@supercat вопросы, которые вы поднимаете, касаются стандарта языка, а не авторов компиляторов. Кроме того, дихотомия не является ложной — возможность игнорировать неопределенные случаи (вместо того, чтобы делать то, что вы хотите) может дать ускорение до 50%. Это действительно проблемы со скоростью, которые превратили стандарт C во что-то, пронизанное неопределенным поведением, а не языковые вандалы. - person toolforger; 27.06.2019
comment
Кстати, это превращается в расширенное обсуждение второстепенных деталей, а не для комментариев. - person toolforger; 27.06.2019
comment
@toolforger: Тогда один короткий прощальный вопрос: как вы думаете, авторы Стандарта намеревались предотвратить использование языка в качестве формы ассемблера высокого уровня? - person supercat; 27.06.2019
comment
Высокоуровневый ассемблер @supercat, безусловно, занимает первое место в списке приоритетов, но, безусловно, есть и другие. Поскольку каждое решение в языковом дизайне является компромиссом, не будет даже четкого языка X, ориентированного на функцию A, когда-либо; это всегда постепенно. - person toolforger; 27.06.2019
comment
Давайте продолжим обсуждение в чате. - person supercat; 27.06.2019

Приятель, если у вас 3 ГБ данных, вам, вероятно, следует решить проблему другим способом, а не заменой элементов данных.

Вместо использования «массива структуры» можно использовать «структуру массивов». Так сказать

struct X
{
    int a;
    double b;
    int c;
};

constexpr size_t ArraySize = 1'000'000;
X my_data[ArraySize];

собирается стать

constexpr size_t ArraySize = 1'000'000;
struct X
{
    int    a[ArraySize];
    double b[ArraySize];
    int    c[ArraySize];
};

X my_data;

Каждый элемент по-прежнему легко доступен mydata.a[i] = 5; mydata.b[i] = 1.5f;....
Отступов нет (кроме нескольких байтов между массивами). Расположение памяти удобно для кэша. Prefetcher выполняет последовательное чтение блоков памяти из нескольких отдельных областей памяти.

Это не так необычно, как может показаться на первый взгляд. Этот подход широко используется для программирования SIMD и GPU.


Массив структур (AoS), Структура массивов

person user3124812    schedule 28.06.2019
comment
Это намного лучше, когда SIMD возможен. Но когда вам нужен разрозненный/произвольный доступ к структурам (и вам нужно несколько членов одной и той же структуры, но ничего ничего из соседних структур), SoA стоит вам в 3 раза меньше кэш-промахов. Это также стоит вам больше указателей/регистров, особенно для не-CISC и/или нестатического распределения. Но если SIMD подходит для любого из ваших циклов, то да, обычно намного лучше иметь SoA. - person Peter Cordes; 16.07.2019

Хотя Стандарт предоставляет реализациям широкое право вставлять произвольное количество пробелов между элементами структуры, это потому, что авторы не хотели пытаться угадать все ситуации, когда отступы могут быть полезны, и принцип «не тратьте пространство без причины». " считалось само собой разумеющимся.

На практике почти каждая стандартная реализация стандартного оборудования будет использовать примитивные объекты, размер которых равен степени двойки, а требуемое выравнивание — степень двойки, не превышающую размер. Кроме того, почти каждая такая реализация помещает каждый элемент структуры в первое доступное кратное его выравнивание, которое полностью следует за предыдущим элементом.

Некоторые педанты будут кричать, что код, использующий такое поведение, является «непереносимым». Им бы я ответил

Код C может быть непереносимым. Хотя комитет C89 стремился дать программистам возможность писать действительно переносимые программы, он не хотел принуждать программистов к переносимости, чтобы предотвратить использование C в качестве «ассемблера высокого уровня»: возможность писать специфичный для машины код одна из сильных сторон С.

В качестве небольшого расширения этого принципа можно сказать, что способность кода, который должен работать только на 90 % машин, использовать функции, общие для этих 90 % машин, — даже если такой код не был бы точно «машинно-специфическим» — одна из сильных сторон C. Представление о том, что программисты на C не должны из кожи вон лезть, чтобы приспособиться к ограничениям архитектур, которые десятилетиями использовались только в музеях, должно быть самоочевидным, но, по-видимому, это не так.

person supercat    schedule 26.06.2019

Вы можете использовать #pragma pack(1), но сама причина этого в том, что компилятор оптимизирует. Доступ к переменной через полный регистр быстрее, чем доступ к ней до наименьшего бита.

Конкретная упаковка полезна только для сериализации и совместимости между компиляторами и т. д.

Как правильно добавил Натан Оливер, это может даже не сработать на некоторых платформах .

person Michael Chourdakis    schedule 25.06.2019
comment
Возможно, вы захотите отметить, что это влечет за собой потенциальные проблемы с производительностью или может привести к тому, что код не будет работать на некоторых платформах: stackoverflow.com/questions/7793511/ - person NathanOliver; 25.06.2019
comment
Насколько мне известно, использование #pragma pack вызывает потенциальные проблемы с производительностью и, как таковое, не является желаемым решением. - person ; 25.06.2019