C неопределенным поведением. Строгое правило алиасинга или неправильное выравнивание?

Я не могу объяснить поведение этой программы при выполнении:

#include <string> 
#include <cstdlib> 
#include <stdio.h>

typedef char u8;
typedef unsigned short u16;

size_t f(u8 *keyc, size_t len)
{
    u16 *key2 = (u16 *) (keyc + 1);
    size_t hash = len;
    len = len / 2;

    for (size_t i = 0; i < len; ++i)
        hash += key2[i];
    return hash;
}

int main()
{
    srand(time(NULL));
    size_t len;
    scanf("%lu", &len);
    u8 x[len];
    for (size_t i = 0; i < len; i++)
        x[i] = rand();

    printf("out %lu\n", f(x, len));
}

Итак, когда он компилируется с -O3 с помощью gcc и запускается с аргументом 25, возникает ошибка сегментации. Без оптимизаций работает нормально. Я разобрал его: он векторизуется, и компилятор предполагает, что массив key2 выровнен по 16 байтам, поэтому он использует movdqa. Очевидно, это UB, хотя я не могу это объяснить. Я знаю о строгом правиле псевдонима, но это не тот случай (надеюсь), потому что, насколько мне известно, правило строгого псевдонима не работает с chars. Почему gcc предполагает, что этот указатель выровнен? Clang тоже отлично работает, даже с оптимизацией.

РЕДАКТИРОВАТЬ

Я изменил unsigned char на char и удалил const, он все еще не работает.

РЕДАКТИРОВАТЬ2

Я знаю, что этот код плохой, но он должен работать нормально, насколько я знаю о строгом правиле псевдонима. Где именно нарушение?


person Nikita Vorobyev    schedule 17.10.2017    source источник
comment
[unsigned] char* имеет особое исключение со строгим псевдонимом: через него можно читать что угодно. Это не бесплатный обход строгого псевдонима, и создание из него невыровненного u16* недопустимо.   -  person Ry-♦    schedule 17.10.2017
comment
Почему вы приводите типы с помощью const? (const u16 *) (keyc + 1);   -  person ryyker    schedule 17.10.2017
comment
@ryyker: Забрасывать const далеко - это плохо.   -  person Ry-♦    schedule 17.10.2017
comment
Вы используете unsigned short* в программе, но нигде нет unsigned short. Это похоже на нарушение псевдонима.   -  person Bo Persson    schedule 17.10.2017
comment
Даже без псевдонима (const u16 *) (keyc + 1) может легко привести к несогласованному доступу. Это очень плохой код.   -  person StoryTeller - Unslander Monica    schedule 17.10.2017
comment
Пожалуйста, исправьте меня, но ваш код не должен компилироваться, почему я думаю, что из-за того, что ваш массив имеет длину 'len', которая является значением времени выполнения (вы устанавливаете значение во время выполнения). Насколько я узнал на C, вы должны затем вызвать malloc или аналогичный для выделения памяти. Типы массивов, их размеры должны быть известны во время компиляции, не так ли?   -  person ExOfDe    schedule 17.10.2017
comment
@ExOfDe - Вы не ошиблись, просто не в курсе. Найдите изменения в C99 в соответствии со стандартом langauge.   -  person StoryTeller - Unslander Monica    schedule 17.10.2017
comment
@ExOfDe В c99 все нормально.   -  person frogatto    schedule 17.10.2017
comment
x - это u8 x[len];, и вы обращаетесь к его членам (char) в функции f через указатель const u16*. Это явное нарушение псевдонима.   -  person PSkocik    schedule 17.10.2017
comment
@ExOfDe, как долго ты программировал на C? Это был стандарт в течение последних 18 лет. (хотя необязательно в течение последних * 6 лет)   -  person Antti Haapala    schedule 17.10.2017
comment
@AnttiHaapala генерирует предупреждение, а предупреждения - это ошибки, поэтому я не знал об этом, кроме того, я кодирую в основном на ANSI C, если C.   -  person ExOfDe    schedule 17.10.2017
comment
Просто чтобы прояснить свою конечную цель, пытаетесь ли вы вычислить хэш C-строки (массив символов с завершающим нулем) или хеш любого объекта, учитывая его двоичное представление в памяти?   -  person Bob__    schedule 17.10.2017
comment
Это ANSI C   -  person Antti Haapala    schedule 17.10.2017
comment
Поведение size_t len; scanf("%lu", &len); зависит от платформы, потому что size_t обычно не такого же размера, как long, что предполагает модификатор типа формата l. Используйте модификатор типа z для ссылки на аргументы типа size_t.   -  person David Foerster    schedule 17.10.2017
comment
Кстати, u8 и u16 - имена типов, которые сильно вводят в заблуждение - они выглядят очень похоже на типы с фиксированной шириной, но таковыми не являются.   -  person Toby Speight    schedule 19.10.2017


Ответы (4)


Код действительно нарушает строгое правило псевдонима. Однако существует не только нарушение псевдонима, и сбой не происходит из-за нарушения псевдонима. Это происходит из-за того, что указатель unsigned short неправильно выровнен; даже само преобразование указателя не определено, если результат не выровнен должным образом.

C11 (черновик n1570), приложение J.2:

1 Поведение не определено в следующих случаях:

....

  • Преобразование между двумя типами указателей дает результат, который неправильно выровнен (6.3.2.3).

С 6.3.2.3p7 говоря

[...] Если результирующий указатель неправильно выровнен [68] для указанного типа, поведение не определено. [...]

unsigned short в вашей реализации (x86-32 и x86-64) требуется выравнивание 2, которое вы можете протестировать с помощью

_Static_assert(_Alignof(unsigned short) == 2, "alignof(unsigned short) == 2");

Однако вы заставляете u16 *key2 указывать на невыровненный адрес:

u16 *key2 = (u16 *) (keyc + 1);  // we've already got undefined behaviour *here*!

Бесчисленное количество программистов настаивают на том, что невыровненный доступ гарантированно работает на практике на x86-32 и x86-64 везде, и на практике не возникнет никаких проблем - ну, все они ошибаются.

Обычно компилятор замечает, что

for (size_t i = 0; i < len; ++i)
     hash += key2[i];

можно более эффективно выполнять с помощью инструкций SIMD, если они соответствующим образом выровнены. Значения загружаются в регистры SSE с использованием _ 7_, что требует, чтобы аргумент был выровнен по 16 байтам:

Если исходный или целевой операнд является операндом памяти, этот операнд должен быть выровнен по 16-байтовой границе, иначе будет сгенерировано исключение общей защиты (#GP).

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

Конечно, если вы начнете с указателя, который указывает на нечетный адрес, даже если не сложить 7 умножить на 2, то получится адрес, выровненный по 16 байтам. Конечно, компилятор даже не сгенерирует код, который обнаружит этот случай, поскольку «поведение не определено, если преобразование между двумя типами указателей дает результат, который неправильно выровнен» - и игнорирует ситуация полностью с непредсказуемыми результатами, что означает, что операнд MOVDQA не будет правильно выровнен, что приведет к затем завершите работу программы.


Легко доказать, что это может происходить даже без нарушения каких-либо строгих правил псевдонима. Рассмотрим следующую программу, которая состоит из 2 единиц перевода (если и f, и вызывающая сторона помещены в одну единицу перевода, мой GCC достаточно умен, чтобы заметить, что мы < em>, используя здесь упакованную структуру, и не генерирует код с MOVDQA):

единица перевода 1:

#include <stdlib.h>
#include <stdint.h>

size_t f(uint16_t *keyc, size_t len)
{
    size_t hash = len;
    len = len / 2;

    for (size_t i = 0; i < len; ++i)
        hash += keyc[i];
    return hash;
}

единица перевода 2

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <inttypes.h>

size_t f(uint16_t *keyc, size_t len);

struct mystruct {
    uint8_t padding;
    uint16_t contents[100];
} __attribute__ ((packed));

int main(void)
{
    struct mystruct s;
    size_t len;

    srand(time(NULL));
    scanf("%zu", &len);

    char *initializer = (char *)s.contents;
    for (size_t i = 0; i < len; i++)
       initializer[i] = rand();

    printf("out %zu\n", f(s.contents, len));
}

Теперь скомпилируйте и свяжите их вместе:

% gcc -O3 unit1.c unit2.c
% ./a.out
25
zsh: segmentation fault (core dumped)  ./a.out

Обратите внимание, что здесь нет нарушения псевдонима. Единственная проблема - это невыровненный uint16_t *keyc.

При -fsanitize=undefined выдается следующая ошибка:

unit1.c:10:21: runtime error: load of misaligned address 0x7ffefc2d54f1 for type 'uint16_t', which requires 2 byte alignment
0x7ffefc2d54f1: note: pointer points here
 00 00 00  01 4e 02 c4 e9 dd b9 00  83 d9 1f 35 0e 46 0f 59  85 9b a4 d7 26 95 94 06  15 bb ca b3 c7
              ^ 
person Antti Haapala    schedule 17.10.2017
comment
Опечатка: (unsigned short *) foo + 1 следует читать (unsigned short *) (foo + 1) - person Joshua; 17.10.2017
comment
Требование выравнивания unsigned short определяется реализацией. Вы говорите внутреннее выравнивание 2, но это утверждение может быть сделано только в контексте конкретной реализации. Документация компилятора OP должна указывать это; а также его можно проверить с помощью _Alignof(unsigned short). Возможно, вы могли бы добавить _Static_assert в свою программу, чтобы подтвердить это - person M.M; 17.10.2017
comment
Я не знаю о среде @Antti, но я заметил, что для gcc 4.8.5 в Linux x86_64 требование выравнивания для unsigned short действительно равно 2. - person John Bollinger; 18.10.2017
comment
Однако вы заставляете u16 * key2 указывать на невыровненный адрес: возможно; keyc + 1 будет невыровнен iff keyc выровнен! - person curiousguy; 22.10.2017
comment
@Flamefire вау, это зло. Возможно, вам стоит добавить ответ. Преимущество C в том, что легко определить, что происходит какая-то злая магия. Не так с C ++ и библиотеками Boost. - person Antti Haapala; 02.08.2019
comment
Готово: stackoverflow.com/a/57326681/1930508. Он получился длиннее, чем я думал, но я надеюсь, что он дает больше идей и примеров, где что-то может потерпеть неудачу. - person Flamefire; 02.08.2019
comment
Почему uint16_t * keyc не привязан? - person Nubcake; 09.04.2020
comment
@Nubcake, потому что это указатель на mystruct член contents, который был вынужден быть невыровненным (путем объявления mystruct с помощью GCC __attribute__((packed))) в другой единице перевода. Компилятор знал, как сгенерировать там код, и не выдавал никаких предупреждений, но к тому времени, когда программа была связана вместе, информация была потеряна. - person Antti Haapala; 09.04.2020
comment
@AnttiHaapala Значит, он обычно выравнивается, если спецификатор Pack не используется, верно? - person Nubcake; 10.04.2020
comment
@Nubcake поздний ответ на комментарий, но да, атрибут packed просит компилятор сознательно нарушить ожидания выравнивания. - person Antti Haapala; 04.04.2021

Допустимо присвоить указателю на объект псевдоним указателя на char, а затем выполнить итерацию всех байтов исходного объекта.

Когда указатель на char фактически указывает на объект (был получен с помощью предыдущей операции), допустимо преобразование обратно в указатель на исходный тип, а стандарт требует, чтобы вы вернули исходное значение.

Но преобразование произвольного указателя на char в указатель на объект и разыменование полученного указателя нарушает правило строгого псевдонима и вызывает неопределенное поведение.

Итак, в вашем коде следующая строка - UB:

const u16 *key2 = (const u16 *) (keyc + 1); 
// keyc + 1 did not originally pointed to a u16: UB
person Serge Ballesta    schedule 17.10.2017
comment
@AnttiHaapala в программе может быть более одного источника UB - person M.M; 17.10.2017

Чтобы предоставить дополнительную информацию и распространенные ошибки отличного ответа от @Antti Haapala:

TL; DR: доступ к невыровненным данным - это неопределенное поведение (UB) в C / C ++. Невыровненные данные - это данные по адресу (также известному как значение указателя), который не делится равномерно на его выравнивание (обычно это его размер). В (псевдо) коде: bool isAligned(T* ptr){ return (ptr % alignof(T)) == 0; }

Эта проблема часто возникает при анализе форматов файлов или данных, отправляемых по сети: у вас есть плотно упакованная структура с разными типами данных. Примером может служить такой протокол: struct Packet{ uint16_t len; int32_t data[]; }; (Читается как: длина 16 бит, за которой следует len, умноженное на 32-битное int в качестве значения). Теперь вы могли:

char* raw = receiveData();
int32_t sum = 0;
uint16_t len = *((uint16_t*)raw);
int32_t* data = (int32_t*)(raw2 + 2);
for(size_t i=0; i<len; ++i) sum += data[i];

Это не работает! Если вы предполагаете, что raw выровнен (по вашему мнению, вы можете установить raw = 0, который выровнен по любому размеру как 0 % n == 0 для всех n), тогда data невозможно выровнять (при условии, что выравнивание == размер типа): len находится по адресу 0, поэтому data находится по адресу 2 и 2 % 4 != 0. Но приведение говорит компилятору: «Эти данные правильно выровнены» («... потому что в противном случае это UB, и мы никогда не столкнемся с UB»). Таким образом, во время оптимизации компилятор будет использовать инструкции SIMD / SSE для более быстрого вычисления суммы, и они дают сбой при получении невыровненных данных.
Примечание: есть невыровненные инструкции SSE, но они медленнее, и, поскольку компилятор предполагает выравнивание, которое вы обещали им здесь не используются.

Вы можете увидеть это в примере из @Antti Haapala, который я сократил и наложил на Godbolt, чтобы вы могли поиграть: https://godbolt.org/z/KOfi6V. Смотрите «программа вернула: 255» или «разбилась».

Эта проблема также довольно часто встречается в процедурах десериализации, которые выглядят следующим образом:

char* raw = receiveData();
int32_t foo = readInt(raw); raw+=4;
bool foo = readBool(raw); raw+=1;
int16_t foo = readShort(raw); raw+=2;
...

read* заботится о порядке байтов и часто реализуется следующим образом:

int32_t readInt(char* ptr){
  int32_t result = *((int32_t*) ptr);
  #if BIG_ENDIAN
  result = byteswap(result);
  #endif
}

Обратите внимание, как этот код разыменовывает указатель, который указывает на меньший тип, который может иметь другое выравнивание, и вы сталкиваетесь с конкретной проблемой.

Эта проблема настолько распространена, что даже Boost страдает от нее во многих версиях. Существует Boost.Endian, который предоставляет простые типы байтов. Код на C от Godbolt можно легко записать как это:

#include <cstdint>
#include <boost/endian/arithmetic.hpp>


__attribute__ ((noinline)) size_t f(boost::endian::little_uint16_t *keyc, size_t len)
{
    size_t hash = 0;
    for (size_t i = 0; i < len; ++i)
        hash += keyc[i];
    return hash;
}

struct mystruct {
    uint8_t padding;
    boost::endian::little_uint16_t contents[100];
};

int main(int argc, char** argv)
{
    mystruct s;
    size_t len = argc*25;

    for (size_t i = 0; i < len; i++)
       s.contents[i] = i * argc;

    return f(s.contents, len) != 300;
}

Тип little_uint16_t - это, по сути, всего лишь несколько символов с неявным преобразованием из / в uint16_t с byteswap, если текущее соответствие машины BIG_ENDIAN. Под капотом код, используемый Boost: endian, был похож на этот:

class little_uint16_t{
  char buffer[2];
  uint16_t value(){
    #if IS_x86
      uint16_t value = *reinterpret_cast<uint16_t*>(buffer);
    #else
    ...
    #endif
    #if BIG_ENDIAN
    swapbytes(value);
    #endif
    return value;
};

Он использовал знание того, что на архитектурах x86 невыровненный доступ возможен. Загрузка с невыровненного адреса была немного медленнее, но даже на уровне ассемблера такая же, как и загрузка с выровненного адреса.

Однако «возможный» не означает действительный. Если компилятор заменил "стандартную" загрузку инструкцией SSE, тогда это не сработает, как можно увидеть на godbolt. Это долгое время оставалось незамеченным, потому что эти инструкции SSE просто используются при обработке больших блоков данных с помощью одной и той же операции, например добавление массива значений, что я и сделал для этого примера. Это было исправлено в , который можно перевести в 1.storeferrer с помощью Boost 1.64 «стандартная» инструкция загрузки в ASM, которая поддерживает выровненные и невыровненные данные на x86, поэтому нет замедления по сравнению с версией с приведением. Но его нельзя преобразовать в выровненные инструкции SSE без дополнительных проверок.

Вывод: не используйте быстрые клавиши с приведением типов. С подозрением относитесь к каждому приведению типов, особенно при приведении к меньшему типу, и убедитесь, что выравнивание не может быть неправильным, или используйте безопасный memcpy.

person Flamefire    schedule 02.08.2019
comment
Эта проблема также довольно часто встречается в процедурах десериализации ... И поэтому это хороший пример классического компромисса правильность / эффективность / удобочитаемость. Если вместо этого вы напишете код десериализации для чтения по одному байту с помощью getc, затем вручную соберите их в многобайтовые слова (см. здесь для некоторых примеров), вы получаете код, который (а) не имеет возможности невыровненного доступа и (б) автоматически работает независимо от порядка байтов хоста (без дополнительной явной перестановки байтов), хотя он (c) вероятно, не максимально эффективен. - person Steve Summit; 23.02.2021
comment
В таком случае, как мы можем достичь того же результата, не копируя данные? Все остальные решения используют копию одного байта / char / uint8_t за раз или _1 _... - person Alexis; 01.04.2021
comment
Почему вы говорите, что невыровненные инструкции SSE работают медленнее? Если посмотреть на внутренние компоненты Intel, _mm_loadu_si128 и _mm_load_si128 имеют одинаковую задержку и пропускную способность для всех их архитектур. - person Alexis; 01.04.2021
comment
Без копии это сделать невозможно. Посмотрите на std :: bit_cast, который по сути является memcpy. Хорошая новость заключается в том, что компилятор может исключить копию и, например, использовать невыровненную нагрузку. Я сказал, что загрузка с невыровненного адреса была немного медленнее. Так что это может больше не соответствовать действительности. Или может. Измерьте, чтобы быть уверенным. Причина того, что он работает медленнее, заключается в том, что ему необходимо загружать из 2 ячеек памяти / выполнять 2 запроса на загрузку на самом низком уровне (микрокод) вместо 1. Это может быть скрыто кешами и т. Д., Но не гарантируется, и первая загрузка может есть дополнительная задержка. Снова: Могу - person Flamefire; 03.04.2021
comment
@Alexis: В Nehalem и новее, movdqu - это та же скорость, что и movdqa для выровненных загрузок (или действительно для любых загрузок, которые не пересекают границу строки кеша). movdqu действительно имеет большую задержку и худшую пропускную способность при разделении строки кэша и намного хуже при разделении страницы. ((а не просто сбой). Кроме того, без AVX только _mm_load_si128 может складываться в источник памяти для инструкции ALU, такой как paddd xmm0, [rdi]. С loadu компилятору потребуется movdqu xmm1, [rdi] / paddd xmm0, xmm1. (С AVX операнды памяти не требуют выравнивания по по умолчанию, только для vmovdqa.) - person Peter Cordes; 23.04.2021
comment
@Flamefire: в GNU C вы можете использовать указатели typedef uint32_t unaligned_aliasing_u32 __attribute__((aligned(1), may_alias)) в качестве альтернативы memcpy. Но да, этот вопрос на самом деле является дубликатом Почему невыровненный доступ к памяти mmap на AMD64 иногда дает сбои? и Pascal Cuoq's блог GCC всегда предполагает доступ к выровненному указателю и pzemtsov.github. io / 2016/11/06 / bug-story-alignment-on-x86.html - person Peter Cordes; 23.04.2021
comment
Конечно, но это не стандартный C / C ++ и непереносимый, поэтому на самом деле не помогает, если вам не нужно быть переносимым (чего я бы не советовал, поскольку memcpy, вероятно, генерирует тот же код) - person Flamefire; 23.04.2021
comment
@PeterCordes Я вижу, но в этом случае я думаю, что причиной задержки является доступ к памяти / кешу, а не сама инструкция. И намного выше, чем еще несколько инструкций. - person Alexis; 23.04.2021
comment
@Flamefire: Верно, в чистом ISO C / C ++ вам нужен memcpy. При включенной оптимизации современные компиляторы почти всегда хорошо справляются с ней, и обычно производительность в режиме отладки не имеет значения. - person Peter Cordes; 23.04.2021

Если код не делает что-то, чтобы гарантировать, что массив символьного типа выровнен, он не должен особенно ожидать, что это будет.

Если о выравнивании позаботились, код принимает его адрес один раз, преобразует его в указатель другого типа и никогда не обращается к хранилищу никакими средствами, не производными от последнего указателя, тогда реализация, предназначенная для низкоуровневого программирования, не должна иметь особого сложность обращения с хранилищем как с абстрактным буфером. Поскольку такое лечение не будет трудным и будет необходимо для некоторых видов низкоуровневого программирования (например, реализация пулов памяти в контекстах, где malloc () может быть недоступен), реализация, которая не поддерживает такие конструкции, не должна претендовать на то, чтобы быть подходящей. для низкоуровневого программирования.

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

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

person supercat    schedule 04.10.2018
comment
Интересный факт: x86-64 System V ABI гарантирует 16-байтовое выравнивание для VLA и локальных / глобальных массивов размером 16 и более байтов. (В этом стандарте очень странно говорить что-либо о внутреннем устройстве функции, поскольку это не похоже на то, что другая функция может знать, что ей был передан указатель на локальный массив, а не на один его элемент.) Итак, в этом случае, компиляция с использованием x86-64 GCC действительно гарантирует 16-байтовое выравнивание x[len] и, следовательно, смещение (u16 *) (keyc + 1). - person Peter Cordes; 23.04.2021
comment
@PeterCordes: Мне интересно, какой ретроним следует использовать для различения диалектов C, которые были полезны, потому что они пытались заполнить части языка тем, что лучше всего подходит для целевой платформы и области приложения, от тех, которые интерпретируют отказ Стандарта к предписывать поведение для construct как приглашение обрабатывать их бессмысленно? На некоторых платформах может быть выгодно не выравнивать символьные массивы, но никогда не было никаких причин для небезупречной реализации принять умышленную слепоту gcc / clang в отношении получения адресов перекрестного типа. - person supercat; 23.04.2021
comment
Если вам нужен невыровненный u16 с защитой от псевдонимов, вы можете ввести его с помощью __attribute__((aligned(1), may_alias)). GNU C предоставляет вам инструменты. Возможно, было бы неплохо, если бы он спас вас от самого себя, как это иногда бывает с _mm256_store_si256, для локального массива, который не использовал alignas (он выбирает выравнивание назначения для производительности, что также позволяет избежать ошибок сегментации из-за смещения vmovdqa. godbolt.org/z/osW5zEefc) - person Peter Cordes; 23.04.2021
comment
Но лучше ли создать ситуацию, когда перенос определения f в другой файл (где он не будет встроен в основной без LTO) сломает программу? Или когда вы меняете буфер, чтобы он выделялся каким-то образом, который скрывает информацию о выравнивании от GCC? Так что у прощения есть свои недостатки. (Если вы имеете в виду, что GCC никогда не должен автоматически векторизоваться таким образом, чтобы он полагался на u16*, имеющий alignof(u16), на целях, где вы можете уйти с невыровненными скалярами, один встречный аргумент - это одобряет или упрощает написание кода, который трудно переносить к требуемым ISA.) - person Peter Cordes; 23.04.2021
comment
@PeterCordes: Я не думаю, что следует полагаться на выравнивание массивов, если только кто-то не предпримет никаких действий, чтобы сделать их такими. Что касается псевдонима, причина, по которой в Стандарте явно не говорится, что правило строгого псевдонима не применяется в случаях, когда, например, T*, который указывает на T, преобразуется в U* и используется для доступа к хранилищу без каких-либо промежуточных операций, связанных с T, заключается в том, что в 1989 году все признали, что ни один компилятор, автор которого не был намеренно тупым, не имел бы никаких проблем с распознаванием таких конструкций. - person supercat; 23.04.2021