Почему теперь существует разница между "{static const char a [] = {...}" и "{const char a [] = {...}"?

Посмотрите на этот крошечный фрагмент кода C или кода C++ на Godbolt...

void b( char const *c);

void a(void)
{
   char const z[] = {0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf, 0xa};

   b(z);
}

void c(void)
{
   static char const z[] = {0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf, 0xa};

   b(z);
}

Более ранние версии gcc компилируют a() и c() в две инструкции, загружают адрес z, переходят в b.

Все современные компиляторы, которые я пытался "пессимизировать" a(), чтобы "создать кадр стека, скопировать z в стек, вызвать b, разбить кадр стека, но оставить c() как простую версию с двумя инструкциями.

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

У кого-нибудь есть идеи, почему?

Ответ 1

C++ имеет следующее правило:

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

Теперь проверьте этот код:

#include <stdio.h>

void c();

void b(const char *a) {
    static const char *p = 0;

    if (!p) {
        p = a;
        c();
    } else {
        if (a==p) {
            printf("problem!\n");
        }
    }
}

void c() {
    const char a[] = { 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf };

    b(a);
}

int main() {
    c();
}

Здесь c вызывается рекурсивно один раз, поэтому в соответствии с правилом массив a должен иметь разные адреса на каждом уровне рекурсии. b сохраняет a при первом вызове и при втором вызове проверяет, является ли он тем же или нет. С соответствующим компилятором он не должен печатать "проблема!". Но на самом деле, со старым компилятором (GCC 4.1, clang 6.0) он выдает "проблему!", Поэтому эти компиляторы нарушают стандарт.

Компилятор разрешено делать статический только в том случае, если оно может быть доказано, что это изменение не a наблюдаемо:

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

Ответ 2

Я ожидаю, что ответом будет то, что компилятор делает то, что вы указываете в своем коде, должно произойти - должен быть локальный массив функций автоматического хранения, который не используется совместно с другими потоками, то есть должен передаваться другим функциям. Ранее компилятор мог использовать правило "как будто", чтобы удалить это и поместить его в другое место, поскольку у языка не было потоков, как существовавших в его модели, но поскольку потоки теперь присутствуют, он должен убедиться, что он случайно не вызывает ложь делиться с другими. Возможно, это могло бы сделать его локальным для потока, но это хуже, чем просто функция local.

Обратите внимание, что GCC никогда не проводил оптимизацию, но Clang прекратил это делать после 6.0.0. Возможно, это ошибка Clang, которая использовала эту оптимизацию.