Возможно ли, чтобы строго в соответствии с положениями стандарта C11 (или выше) эта программа могла быть оптимизирована для печати hello?

#c #c11 #strict-aliasing

Вопрос:

Учитывая эту программу, строгие правила сглаживания и оптимизация анализа псевдонимов на основе типов:

 #include<stdio.h>
#include<stddef.h>
#include<stdalign.h>
#include<assert.h>

struct thing {
  int x;
  int y;
};

static_assert(sizeof("hello") < sizeof(struct thing),"buffer can't hold text");

int main() {

    alignas(struct thing) char buffer[sizeof(struct thing)] = "hello";
    void *ptr = buffer;
    struct thing* s = ptr;
    
    s->x = 20;
    s->y = 30;

    printf("%s",buffer);

    return 0;
}
 

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

Возможно, я добавлю мотивирующий пример того, почему кто-то хотел бы это сделать. Допустим, кто-то создал распределитель арены, который отлично работает, когда мэллок передает начальный блок памяти. Нет проблем, каждый раз, когда мы записываем в память, полученную из этого распределителя, мы меняем эффективные типы при первой записи в (void*), которую мы получаем. Однако, если наш распределитель арены инициализируется символом*, полученным из стековой памяти (скажем, выровненным соответствующим образом для любого типа, который мы будем в него записывать), это большое нет, так как мы будем сглаживать буфер символов.

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

Комментарии:

1. Поправьте меня, если я ошибаюсь, но похоже, что программа имеет UB и поэтому может печатать все, что захочет.

2. @n.1.8e9-где мой sharem.: Стандарт не предназначен для полного описания всего необходимого, чтобы сделать реализацию подходящей для каких-либо конкретных задач. Вместо этого он ожидает, что реализации, предназначенные для различных целей, расширят семантику языка, указав поведение в большем количестве случаев, чем предписано. Реализации, подходящие для низкоуровневого программирования, расширяют язык таким образом, а реализации, которые не поддерживают абсолютно ничего, кроме того, что требуется Стандартом, непригодны для низкоуровневого программирования.

3. @supercat Ты мне объясняешь, что значит «неопределенное поведение»? Я знаю.

4. @n.1.8e9-где моя доля.: При педантичном чтении правил большинство не придуманных программ нарушают ограничения N1570 6.5p7. Вопрос о том, какие программы следует осмысленно обрабатывать, остается вопросом качества реализации. Все не придуманные компиляторы расширяют язык, чтобы с пользой поддерживать конструкции, например struct foo someStruct = {1,someFunction()}; , в автоматическом режиме, или someStruct.intArray[n] = 23; даже если оба получают доступ к объекту типа struct foo в течение его срока службы, но не через значение lvalue совместимого типа. С другой стороны, ни лязга, ни gcc…

5. …поддерживает все угловые случаи, предусмотренные Стандартом, за исключением случаев, когда вызывается с -fno-strict-aliasing помощью . Если рассматривать эту сноску в подразумевая, что качество реализации должны лишь толковать нормы, как говорят, когда компиляторы должны предположить, что вещи могут псевдоним , даже если нет других конкретных доказательств того, что они сделали бы так , что эта конструкция должна иметь никаких проблем, но потом снова тоже самое для конструкции разработчики Clang и gcc отказаться./

Ответ №1:

Выравнивание-не единственная проблема здесь. Эффективным типом buffer является массив символов, но вы предоставляете доступ к значению как int . Это довольно явное нарушение строгого сглаживания и неопределенное поведение. Может случиться все, что угодно.

«Что угодно» включает в s->x = 20; себя запись, или оптимизацию, или сбой программы, потому что строка была выделена в разделе «только для чтения», или сбой программы, но строка осталась нетронутой, потому что она была выделена в режиме реального времени, например, во флэш-памяти.

Комментарии:

1. Что касается «соответствующего компилятора», единственное требование состоит в том, что он должен принимать строго соответствующую программу. И это не так, поскольку оно содержит неопределенное поведение.

2. Разве не разрешено записывать любой тип в char буфер? Будет ли это иметь значение?

3. @TedLyngmo Нет, все наоборот. Вам разрешено проверять любой тип байт за байтом с помощью символьного указателя. И в правилах псевдонимов есть исключение из доступа к значению lvalue, когда это делается с помощью символьного типа. Ваш пример-другое исключение, это «тип агрегата или объединения», который также содержит исключение в списке (C17 6.5).

4. Это тот ответ, которого я ожидал. Похоже, большая проблема в том, что по существу нельзя использовать «хранилище, выделенное стеком» (без обмана объединения), потому что оно имеет объявленный тип, который может вмешиваться с помощью строгих правил псевдонима. Я хотел бы, чтобы это можно было изменить путем выделения стека без объявленного типа. Т. е. выравнивание(обязательно) нетипизированного буфера[размер]. Это будет вести себя так, как если бы это было результатом malloc, но с установленным требованием выравнивания. Разумный вариант использования для этого, например, скажем, у вас есть распределитель арены, который отлично работает с хранилищем из malloc, почему он не работает с правильно выровненным стеком

5. @anonymouscoward Нестандартные функции, такие как alloca, могут работать подобным образом. По сути, они просто увеличивают указатель стека на несколько байтов. Но лучше всего использовать -fno-strict-aliasing gcc или просто держаться подальше от него. Большинство проблем со злоупотреблением псевдонимами в gcc были исправлены задолго до выпуска C11, но людям иногда удается создавать сценарии, в которых он все еще ломается. Все дело сводится к недостаткам в языковом стандарте.

Ответ №2:

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

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