Что такое строгое правило псевдонимов?

661

Когда вы спрашиваете о общем поведении undefined в C, души более просвещены, чем я говорил о правиле строгого псевдонимов.
О чем они говорят?

  • 11
    @Ben Voigt: правила псевдонимов различны для c ++ и c. Почему этот вопрос помечен c и c++faq .
  • 4
    @MikeMB: Если вы проверите историю, вы увидите, что я сохранил теги такими, какими они были изначально, несмотря на попытки некоторых экспертов изменить вопрос из-под существующих ответов. Кроме того, зависимость от языка и версии является очень важной частью ответа на вопрос «Что такое строгое правило наложения имен?» и знание различий важно для команд, которые переносят код между C и C ++ или пишут макросы для использования в обоих.
Показать ещё 5 комментариев
Теги:
undefined-behavior
strict-aliasing
type-punning
c++-faq

11 ответов

501
Лучший ответ

Типичная ситуация, с которой вы сталкиваетесь при строгих проблемах с псевдонимом, - это наложение структуры (например, устройства/сети msg) на буфер размера слова вашей системы (например, указатель на uint32_t или uint16_t s). Когда вы накладываете структуру на такой буфер или буфера на такую структуру через кастинг, вы можете легко нарушать правила строгого сглаживания.

Поэтому в такой настройке, если я хочу отправить сообщение на что-то, мне придется иметь два несовместимых указателя, указывающих на один и тот же кусок памяти. Тогда я мог бы наивно закодировать что-то вроде этого:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Правило строгого псевдонима делает эту установку незаконной: разыменование указателя, который псевдонизирует объект, который не соответствует совместимому типу, или один из других типов, разрешенных C 2011 6.5 пунктом 7 1, является неопределенным поведением. К сожалению, вы все равно можете запрограммировать этот способ, может быть, получить некоторые предупреждения, скомпилировать его, только чтобы иметь странное неожиданное поведение при запуске кода.

(GCC выглядит несколько непоследовательным в своей способности давать предупреждения на псевдонимы, иногда давая нам дружеское предупреждение, а иногда и нет).

Чтобы понять, почему это поведение не определено, мы должны подумать о том, что правило строкового aliasing покупает компилятор. В принципе, с этим правилом, ему не нужно думать о вставке инструкций, чтобы обновлять содержимое buff каждый цикл цикла. Вместо этого при оптимизации с некоторыми досадными неулучшаемыми предположениями об aliasing он может опустить эти инструкции, load buff[0] и buff[1 ] в регистры CPU один раз до того, как цикл будет запущен, и ускорить тело цикла. Прежде чем вводить строгий псевдоним, компилятор должен был жить в состоянии паранойи, чтобы содержимое buff могло меняться в любое время из любого места кем угодно. Таким образом, чтобы получить дополнительную отдачу от производительности, и если большинство людей не набирают указатели на каламбуры, было введено строгое правило псевдонимов.

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

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

И переписал наш более ранний цикл, чтобы воспользоваться этой удобной функцией

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Компилятор может быть или не быть способен или достаточно умен, чтобы попытаться встроить SendMessage, и он может или не может снова загрузить или не загружать бафф. Если SendMessage является частью другого API, который скомпилирован отдельно, он, вероятно, имеет инструкции для загрузки содержимого буфера. Опять же, возможно, вы находитесь в C++, и это некоторая шаблонная версия только для заголовка, которую компилятор считает встроенным. Или, может быть, это просто то, что вы написали в своем.c файле для вашего удобства. В любом случае может возникнуть неопределенное поведение. Даже когда мы знаем некоторые из того, что происходит под капотом, это по-прежнему является нарушением правила, поэтому не гарантируется четкое поведение. Так что просто обертывание в функцию, которая берет наш буфер с разделителями слов, не обязательно помогает.

Итак, как мне обойти это?

  • Используйте союз. Большинство компиляторов поддерживают это, не жалуясь на строгий псевдоним. Это разрешено на C99 и явно разрешено в C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Вы можете отключить строгое сглаживание в своем компиляторе (f [no-] строгое сглаживание в gcc))

  • Вы можете использовать char* для сглаживания вместо системного слова. Правила допускают исключение для char* (включая signed char и unsigned char). Он всегда предполагал, что char* псевдонимы других типов. Однако это не сработает по-другому: нет предположения, что ваша структура псевдонизирует буфер символов.

Начинающий остерегайтесь

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

сноска

1 Типы C 2011 6.5 7 позволяют получить доступ к lvalue:

  • тип, совместимый с эффективным типом объекта,
  • квалифицированную версию типа, совместимую с эффективным типом объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим эффективному типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии эффективного типа объекта,
  • совокупный или союзный тип, который включает один из вышеупомянутых типов среди его членов (включая рекурсивно, член субагрегата или объединенного объединения) или
  • тип символа.
  • 13
    Кажется, я иду после битвы ... может ли unsigned char* использоваться вместо char* ? Я склонен использовать unsigned char вместо char в качестве базового типа для byte потому что мои байты не подписаны, и я не хочу, чтобы странность подписанного поведения (особенно в отношении переполнения)
  • 29
    @Matthieu: подпись не имеет никакого значения для правил псевдонимов, поэтому использование unsigned char * нормально.
Показать ещё 39 комментариев
229

Лучшее объяснение, которое я нашел, - Майк Актон, Понимание строго алиасинга. Он немного сосредоточился на разработке PS3, но в основном это просто GCC.

Из статьи:

"Строгое сглаживание - это предположение, сделанное компилятором C (или С++), что указатели на разузнавание объектов разных типов никогда не будут ссылаться на одно и то же место памяти (то есть псевдонимы друг друга.)"

Итак, в основном, если у вас есть int*, указывающий на некоторую память, содержащую int, а затем вы указываете a float* в эту память и используете ее как float, вы нарушаете правило. Если ваш код не соответствует этому, оптимизатор компилятора, скорее всего, нарушит ваш код.

Исключением из правила является char*, которому разрешено указывать любой тип.

  • 6
    Так какой же канонический способ легально использовать одну и ту же память с переменными 2 разных типов? или все просто копируют?
  • 4
    Но тогда вы не можете иметь союзы.
Показать ещё 18 комментариев
119

Это правило строгого сглаживания, которое содержится в разделе 3.10 стандарта С++ 03 (другие ответы дают хорошее объяснение, но никто не предоставил это правило):

Если программа пытается получить доступ к сохраненному значению объекта через значение l, отличного от одного из следующих типов, поведение undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
  • совокупность или тип объединения, который включает один из вышеупомянутых типов среди своих членов (включая рекурсивно, член подгруппы или объединенный союз),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или unsigned char.

С++ 11 и C++ 14 (подчеркнуты изменения):

Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) для динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или unsigned char.

Два изменения были небольшими: glvalue вместо lvalue и уточнение случая агрегата/объединения.

Третье изменение делает более сильную гарантию (ослабляет правило сильного aliasing): новая концепция подобных типов, которые теперь безопасны для псевдонимов.


Кроме того, формулировка C (C99; ISO/IEC 9899: 1999 6.5/7, точно такая же формулировка используется в ISO/IEC 9899: 2011 §6.5 ¶7):

Объект должен иметь сохраненное значение, доступное только с помощью значения lvalue выражение, имеющее один из следующих типов 73) или 88):

  • тип, совместимый с эффективным типом объекта,
  • квалифицированная версия типа, совместимого с эффективным типом объект,
  • тип, который является подписанным или неподписанным типом, соответствующим эффективный тип объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квалифицированная версия эффективного типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типы среди своих членов (в том числе, рекурсивно, член subaggregate или contains union) или
  • тип символа.

73) или 88) Цель этого списка - указать те обстоятельства, при которых объект может или не может быть сглажен.

  • 7
    Бен, поскольку людей часто направляют сюда, я позволил себе добавить ссылку на стандарт Си также для полноты картины.
  • 0
    @Kos: Это круто, спасибо! Вы также можете прокомментировать, требовал ли C89 / 90 строгого алиасинга? (Кажется, я не помню, чтобы оно было введено одновременно с ключевым словом restrict , но я не уверен).
Показать ещё 33 комментария
38

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

  • 3
    « Строгое псевдонимы относятся не только к указателям, но и к ссылкам ». На самом деле это относится к lvalues . « Использование memcpy - единственное исправление портативного » Слушай!
  • 3
    Раздел «Другая сломанная версия, ссылаясь на дважды» не имеет смысла. Даже если бы была точка последовательности, это не дало бы правильного результата. Возможно, вы хотели использовать операторы сдвига вместо назначений смен? Но тогда код четко определен и делает правильные вещи.
Показать ещё 12 комментариев
30

В качестве дополнения к тому, что писал Дуг Т., здесь это простой тестовый пример, который, вероятно, запускает его с помощью gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Скомпилируйте с помощью gcc -O2 -o check check.c. Обычно (с большинством версий gcc, которые я пробовал) это выводит "жесткую проблему псевдонимов", потому что компилятор предполагает, что "h" не может быть тем же адресом, что и "k" в функции "проверка". Из-за этого компилятор оптимизирует if (*h == 5) и всегда вызывает printf.

Для тех, кто интересуется здесь, ассемблерный код x64, созданный gcc 4.6.3, работает на ubuntu 12.04.2 для x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Итак, условие if полностью исчезло из кода ассемблера.

  • 0
    если вы добавите второе короткое значение * j в check () и используете его (* j = 7), оптимизация исчезнет, поскольку ggc не исчезнет, если h и j не указывают на одно и то же значение. да оптимизация действительно умная.
  • 2
    Чтобы сделать вещи более увлекательными, используйте указатели на типы, которые не совместимы, но имеют одинаковый размер и представление (в некоторых системах это верно, например, long long* и int64_t *). Можно было бы ожидать, что здравомыслящий компилятор должен признать, что long long* и int64_t* могут обращаться к одному и тому же хранилищу, если они хранятся одинаково, но такое обращение уже не модно.
28

Заметка

Это выдержка из моего "Что такое строгое правило сложения и почему мы заботимся"? записать.

Что такое строгий псевдоним?

В C и C++ aliasing имеет отношение к тем, к каким типам выражений нам разрешен доступ к хранимым значениям через. В C и C++ стандарт указывает, какие типы выражений разрешены для псевдонимов, какие типы. Компилятору и оптимизатору разрешено считать, что мы строго следуем правилам псевдонимов, следовательно, это правило строгого псевдонима. Если мы попытаемся получить доступ к значению с использованием недопустимого типа, он классифицируется как неопределенное поведение (UB). Как только у нас есть неопределенное поведение, все ставки отключены, результаты нашей программы уже не являются надежными.

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

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

Предварительные примеры

Давайте посмотрим на некоторые примеры, затем мы можем говорить о том, что говорят стандарты (ы), изучать некоторые дополнительные примеры, а затем посмотреть, как избежать строгих псевдонимов и уловов, которые мы пропустили. Вот пример, который не должен удивлять (живой пример):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

У нас есть int *, указывающий на память, занимаемую int, и это действительное псевдонижение. Оптимизатор должен предположить, что присвоения через ip могут обновить значение, занимаемое x.

Следующий пример показывает сглаживание, которое приводит к неопределенному поведению (живой пример):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

В функции foo мы берем int * и float *, в этом примере мы вызываем foo и устанавливаем оба параметра, чтобы указывать на ту же ячейку памяти, которая в этом примере содержит int. Обратите внимание, что reinterpret_cast сообщает компилятору обработать выражение так, как если бы он имел тип, определенный его параметром шаблона. В этом случае мы говорим, что обрабатываем выражение & x, как если бы он имел тип float *. Мы можем наивно ожидать, что результатом второго cout будет 0, но с оптимизацией, включенной с использованием -O2, gcc и clang дают следующий результат:

0
1

Который может и не ожидаться, но совершенно корректен, поскольку мы вызывали неопределенное поведение. Поплавок не может быть действительным псевдонимом для объекта int. Поэтому оптимизатор может считать константу 1, хранящимися при разыменовании я буду возвращаемое значением, так как магазин через F не может законным образом повлиять на Int объекта. Подсоединение кода в Compiler Explorer показывает, что это именно то, что происходит (живой пример):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Оптимизатор, использующий анализ псевдонимов на основе типа (TBAA), предполагает, что 1 будет возвращен и будет напрямую перемещать постоянное значение в регистр eax, который несет возвращаемое значение. TBAA использует правила языков о том, какие типы разрешены для псевдонима, чтобы оптимизировать нагрузки и магазины. В этом случае TBAA знает, что float не может использовать псевдоним и int и оптимизирует загрузку i.

Теперь, в книгу правил

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

Что говорит стандарт C11?

В стандарте C11 говорится следующее в разделе 6.5. Выражения, пункт 7:

Объект должен иметь сохраненное значение, доступ к которому можно получить только с помощью выражения lvalue, которое имеет один из следующих типов: 88) - тип, совместимый с эффективным типом объекта,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- квалифицированная версия типа, совместимая с эффективным типом объекта,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- тип, который является подписанным или неподписанным типом, соответствующим эффективному типу объекта,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc/clang имеет расширение, а также позволяет присваивать unsigned int * int *, даже если они не совместимы.

- тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии эффективного типа объекта,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- совокупный или тип объединения, который включает один из вышеупомянутых типов среди его членов (включая рекурсивно, член субагрегата или содержащегося объединения) или

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- тип символа.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Что говорится в [8]

Проект C++ 17 в разделе [basic.lval] в пункте 11 гласит:

Если программа пытается получить доступ к сохраненному значению объекта с помощью glvalue другого, чем один из следующих типов, поведение не определено: 63 (11.1) - динамический тип объекта,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - cv-квалифицированная версия динамического типа объекта,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - тип, аналогичный (как определено в 7.5) для динамического типа объекта,

(11.4) - тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - тип, который является подписанным или неподписанным типом, соответствующим стандартной версии динамического типа объекта cv,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - совокупный или объединенный тип, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - символ char, unsigned char или std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Стоит отметить, что подписанный символ не включен в список выше, это заметное отличие от C, в котором указан тип символа.

Что такое Type Punning

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

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

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

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Как мы видели ранее, это не является допустимым псевдонимом, поэтому мы вызываем неопределенное поведение. Но традиционно компиляторы не воспользовались строгими правилами псевдонимов, и этот тип кода, как правило, просто работал, разработчики, к сожалению, привыкли делать это таким образом. Общим альтернативным методом для punning типа является объединение, которое действительно в C, но неопределенное поведение в C++ (см. Живой пример):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n", u.n );  // UB in C++ n is not the active member

Это недействительно в C++, а некоторые считают, что профсоюзы должны быть исключительно для реализации вариантов вариантов и чувствовать, что использование союзов для типа punning является злоупотреблением.

Как правильно ввести Pun Pun?

Стандартный метод для пиннинга типа в C и C++ - memcpy. Это может показаться немного тяжелым, но оптимизатор должен распознать использование memcpy для тиража и оптимизировать его и создать регистр для регистрации перемещения. Например, если мы знаем, что int64_t имеет тот же размер, что и double:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

мы можем использовать memcpy:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

На достаточном уровне оптимизации любой достойный современный компилятор генерирует идентичный код для ранее упомянутого метода reinterpret_cast или метода объединения для пин-кода типа. Изучая сгенерированный код, мы видим, что он использует только register mov (пример Live Compiler Explorer).

C++ 20 и bit_cast

В C++ 20 мы можем получить bit_cast (реализация доступна в ссылке из предложения), которая дает простой и безопасный способ ввода слов, а также может использоваться в контексте constexpr.

Ниже приведен пример использования bit_cast для ввода pun для unsigned int для float (см. Его в прямом эфире):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

В случае, когда типы To и From не имеют одинакового размера, нам требуется использовать промежуточную структуру15. Мы будем использовать структуру, содержащую массив символов sizeof (unsigned int) (предполагается, что 4 байта без знака int) является типом From и unsigned int в качестве типа.

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

К сожалению, нам нужен этот промежуточный тип, но это текущее ограничение bit_cast.

Поймать строгие нарушения алиментации

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

gcc, используя флаг -fstrict-aliasing и -Wstrict-aliasing, может поймать некоторые случаи, хотя не без ложных срабатываний/негативов. Например, следующие случаи будут генерировать предупреждение в gcc (см. Его в прямом эфире):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

хотя он не поймает этот дополнительный случай (см. его в прямом эфире):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Хотя clang позволяет эти флаги, по-видимому, на самом деле не реализует предупреждения.

Другим инструментом, который мы имеем для нас, является ASan, который может поймать неверные нагрузки и магазины. Хотя они не являются строго строгими нарушениями псевдонимов, они являются общим результатом нарушений строгих нарушений. Например, следующие случаи будут генерировать ошибки времени выполнения при построении с помощью clang с использованием -fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Последний инструмент, который я порекомендую, является C++ конкретным, а не строго инструментом, а практикой кодирования, не допускайте приведения C-стиля. Как gcc, так и clang будут производить диагностику для C-стиля при использовании -Wold-style-cast. Это заставит любые неопределенные фразы типа использовать reinterpret_cast, в общем случае reinterpret_cast должен быть флагом для более тщательного обзора кода. Также проще выполнить поиск в базе кода для reinterpret_cast для проведения аудита.

Для C у нас есть все инструменты, которые уже были рассмотрены, и у нас также есть tis-interpreter, статический анализатор, который исчерпывающе анализирует программу для большого подмножества языка C. Учитывая C-примеры предыдущего примера, когда использование -fstrict-aliasing пропускает один случай (см. Его в прямом эфире)

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter способен уловить все три, следующий пример вызывает tis-kernal как tis-интерпретатор (вывод редактируется для краткости):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

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

  • 0
    Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
  • 2
    Если бы я мог, +10, хорошо написал и объяснил, также с обеих сторон, писателей компиляторов и программистов ... единственная критика: было бы неплохо иметь контрпримеры выше, чтобы увидеть, что запрещено стандартом, это не очевидно вид :-)
15

Тип punning с помощью указателей (в отличие от использования объединения) является основным примером нарушения строгой псевдонимы.

  • 1
    См. Мой ответ здесь для соответствующих кавычек, особенно сносок, но написание шрифтов через союзы всегда было разрешено в C, хотя сначала это было плохо сформулировано. Вы хотите уточнить свой ответ.
  • 0
    @ShafikYaghmour: C89 явно позволял разработчикам выбирать случаи, в которых они могли бы или не могли бы с пользой распознать наказание за тип через профсоюзы. Реализация могла бы, например, указать, что для записи в один тип с последующим чтением другого должно быть распознано как определение типа, если между записью и чтением программист выполнил одно из следующих действий: (1) вычислить значение lvalue, содержащее тип объединения [получение адреса члена будет соответствовать требованиям, если оно будет выполнено в правильной точке последовательности]; (2) преобразовать указатель на один тип в указатель на другой и получить доступ через этот ptr.
Показать ещё 2 комментария
10

В соответствии с обоснованием C89, авторы Стандарта не хотели требовать, чтобы компиляторы задавали такой код:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

необходимо перезагрузить значение x между оператором присваивания и возврата, чтобы позволить возможность p указывать на x, а присвоение *p может, следовательно, изменить значение of x. Понятие о том, что компилятор должен иметь право предполагать, что в подобных ситуациях не будет сглаживания, не противоречиво.

К сожалению, авторы C89 написали свое правило таким образом, что, если бы его прочитали буквально, он сделал бы даже следующую функцию invoke Undefined Behavior:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

потому что он использует lvalue типа int для доступа к объекту типа struct S, а int не относится к типам, которые можно использовать для доступа к struct S. Поскольку было бы абсурдно относиться ко всем использованиям элементов и объединений несимвольных типов как Undefined Behavior, почти все признают, что существуют, по крайней мере, некоторые обстоятельства, когда lvalue одного типа может использоваться для доступа к объекту другой тип. К сожалению, Комитет по стандартам С не смог определить, каковы эти обстоятельства.

Большая часть проблемы является результатом отчета об ошибке № 028, в котором спрашивается о поведении программы, например:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

В отчете "Ошибка № 28" указано, что программа вызывает Undefined "Поведение", потому что действие записи члена объединения типа "double" и чтение одного из типа "int" вызывает поведение, определяемое реализацией. Такое рассуждение бессмысленно, но составляет основу для правил эффективного типа, которые бесполезно усложняют язык, не делая ничего для решения исходной проблемы.

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

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Не существует конфликта внутри inc_int, потому что все обращения к хранилищу, доступ к которому осуществляется через *p, выполняются с lvalue типа int, и нет конфликта в test, потому что p явно отображается из struct S, и к следующему моменту использования s все обращения к этому хранилищу, которые когда-либо будут выполняться через p, уже произойдут.

Если код немного изменился...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Здесь существует конфликт псевдонимов между p и доступом к s.x в отмеченной строке, поскольку в этой точке выполнения существует еще одна ссылка, которая будет использоваться для доступа к одному и тому же хранилищу.

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

  • 0
    Хорошо, было бы интересно прочитать предложение такого рода, которое было бы более или менее «тем, что мог бы сделать комитет по стандартам», которое достигло бы их целей, не внося такой большой сложности.
  • 1
    @jrh: я думаю, это было бы довольно просто. Признайте, что: 1. Чтобы наложение псевдонимов происходило во время конкретного выполнения функции или цикла, во время этого выполнения необходимо использовать два разных указателя или значения для обращения к одному и тому же хранилищу в конфликтующем фашоне; 2. Признать, что в тех случаях, когда один указатель или lvalue только что визуально получен из другого, доступ ко второму - это доступ к первому; 3. Признать, что правило не предназначено для применения в случаях, которые на самом деле не включают псевдонимы.
Показать ещё 1 комментарий
9

После прочтения многих ответов я чувствую необходимость добавить что-то:

Строгое сглаживание (которое я опишу немного) важно, потому что:

  • Доступ к памяти может быть дорогостоящим (с точки зрения производительности), поэтому данные обрабатываются в регистре CPU, прежде чем они будут возвращены в физическую память.

  • Если данные в двух разных регистрах ЦП будут записаны в одно и то же пространство памяти, , мы не можем предсказать, какие данные "выживут" , когда мы с кодом на C.

    В сборке, где мы будем вручную загружать и выгружать регистры процессора, мы будем знать, какие данные остаются нетронутыми. Но C (к счастью) абстрагирует эту деталь.

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

Этот дополнительный код медленный и повреждает производительность, поскольку он выполняет дополнительные операции чтения/записи в памяти, которые медленнее и (возможно) не нужны.

Правило Strict aliasing позволяет избежать избыточного машинного кода в случаях, когда должно быть безопасным предположить, что два указателя не указывают на один и тот же блок памяти (см. также restrict ключевое слово).

Строгое сглаживание позволяет с уверенностью предположить, что указатели на разные типы указывают на разные местоположения в памяти.

Если компилятор замечает, что два указателя указывают на разные типы (например, int * и a float *), предполагается, что адрес памяти отличается и не будет защищен от столкновений с адресами памяти, что приводит к ускорению машинного кода.

Например:

Предположим, что следующая функция:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Чтобы обрабатывать случай, в котором a == b (оба указателя указывают на одну и ту же память), нам нужно заказать и протестировать способ загрузки данных из памяти в регистры процессора, поэтому код может закончиться это:

  • загрузите a и b из памяти.

  • добавить a в b.

  • сохранить b и перезагрузить a.

    (сохранение из регистров CPU в память и загрузка из памяти в регистр CPU).

  • добавить b в a.

  • сохранить a (из регистра CPU) в память.

Шаг 3 очень медленный, потому что ему нужно получить доступ к физической памяти. Однако для защиты от экземпляров, где a и b указывают на один и тот же адрес памяти, требуется защита.

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

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

    void merge_two_numbers(int *a, long *b) {...}
    
  • Использование ключевого слова restrict. то есть:.

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Теперь, выполнив правило Strict Aliasing, можно избежать шага 3, и код будет работать значительно быстрее.

Фактически, добавив ключевое слово restrict, вся функция может быть оптимизирована для:

  • загрузите a и b из памяти.

  • добавить a в b.

  • сохранить результат как до a, так и b.

Эта оптимизация не могла быть выполнена раньше, из-за возможного столкновения (где a и b были бы утроены, а не удвоены).

  • 0
    с ключевым словом restrict, на шаге 3, разве не следует сохранить результат только в 'b'? Звучит так, как будто результат суммирования будет также сохранен в «а». Нужно ли перезагрузить его снова?
  • 1
    @NeilB - Да, ты прав. Мы сохраняем только b (не перезагружая его) и перезагружая a . Надеюсь, теперь стало понятнее.
Показать ещё 1 комментарий
5

Строгое сглаживание не позволяет использовать разные типы указателей для одних и тех же данных.

Эта статья должна помочь вам разобраться в проблеме подробно.

  • 4
    Вы можете использовать псевдоним между ссылками, а также между ссылкой и указателем. Смотрите мой учебник dbp-consulting.com/tutorials/StrictAliasing.html
  • 4
    Разрешается иметь разные типы указателей на одни и те же данные. Строгий псевдоним возникает тогда, когда одна и та же ячейка памяти записывается через один тип указателя и читается через другой. Также допускаются некоторые различные типы (например, int и структура, которая содержит int ).
-2

Технически в C++ правило строгого сглаживания, вероятно, никогда не применимо.

Обратите внимание на определение оператора косвенности (*):

Оператор унарного * выполняет косвенное обращение: выражение, к которому оно применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, относящееся к объекту или функции, к которой относится выражение.

Также из определения glvalue

Значение gl - выражение, оценка которого определяет идентичность объекта, (... snip)

Таким образом, в любой четко определенной трассировке программы glvalue ссылается на объект. Таким образом, так называемое строгое правило сглаживания не применяется. Возможно, это не то, что хотели дизайнеры.

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню