Подтвердить что ты не робот

Почему современные компиляторы С++ не оптимизируют простые циклы? (Clang, MSVC)

Когда я компилирую и запускаю этот код с помощью Clang (-O3) или MSVC (/O2)...

#include <stdio.h>
#include <time.h>

static int const N = 0x8000;

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i)
    {
        int a[N];    // Never used outside of this block, but not optimized away
        for (int j = 0; j < N; ++j)
        {
            ++a[j];  // This is undefined behavior (due to possible
                     // signed integer overflow), but Clang doesn't see it
        }
    }
    clock_t const finish = clock();
    fprintf(stderr, "%u ms\n",
        static_cast<unsigned int>((finish - start) * 1000 / CLOCKS_PER_SEC));
    return 0;
}

... цикл не оптимизируется.

Кроме того, ни Clang 3.6 и Visual С++ 2013 и GCC 4.8.1 сообщает мне, что переменная неинициализирована!

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

Но только GCC может понять, что это no-op, и ни один из компиляторов не говорит мне, что это неинициализированная переменная.

Почему это? Что мешает анализу простой жизнеспособности сообщить компилятору, что a не используется? Более того, почему компилятор не обнаруживает, что a[j] неинициализирован в первую очередь? Почему существующие неинициализированные переменные-детекторы во всех этих компиляторах не улавливают эту очевидную ошибку?

4b9b3361

Ответ 1

Поведение undefined здесь не имеет значения. Замена внутреннего контура на:

    for (int j = 1; j < N; ++j)
    {
        a[j-1] = a[j];
        a[j] = j;
    }

... имеет тот же эффект, по крайней мере, с Clang.

Проблема заключается в том, что внутренний цикл загружается из a[j] (для некоторого j) и сохраняется в a[j] (для некоторого j). Ни один из хранилищ не может быть удален, поскольку компилятор считает, что они могут быть видны для более поздних нагрузок, и ни одна из нагрузок не может быть удалена, потому что их значения используются (как входные данные в более поздние магазины). В результате цикл все еще имеет побочные эффекты в памяти, поэтому компилятор не видит, что его можно удалить.

В отличие от n.m. ответ, заменив int на unsigned, проблема не исчезнет. Код, созданный Clang 3.4.1 с использованием int и с помощью unsigned int является идентичным.

Ответ 2

Это интересная проблема оптимизации. я бы ожидайте, что в большинстве случаев компилятор будет обрабатывать каждый элемент массива как индивидуальная переменная при выполнении мертвого кода анализ. Ans 0x8000 делают слишком много отдельных переменных для трек, поэтому компилятор не пытается. Тот факт, что a[j] не всегда доступ к одному и тому же объекту может вызвать проблемы а также для оптимизатора.

Очевидно, что разные компиляторы используют разные эвристики; компилятор может рассматривать массив как один объект и обнаруживать что он никогда не влиял на результат (наблюдаемое поведение). Некоторые компиляторы могут не делать этого, однако, на том основании, что как правило, это большая работа для очень небольшого выигрыша: как часто будут ли такие оптимизации применяться в реальном коде?

Ответ 3

++a[j];  // This is undefined behavior too, but Clang doesn't see it

Вы говорите, что это поведение undefined, потому что элементы массива неинициализированы?

Если это так, хотя это стандартная интерпретация пункта 4.1/1 в стандарте, я считаю, что это неверно. Элементы "неинициализированы" в том смысле, что программисты обычно используют этот термин, но я не верю, что это точно соответствует спецификации языка С++.

В частности, в С++ 11 8.5/11 указано, что эти объекты фактически инициализируются по умолчанию, и это кажется мне взаимоисключающим, поскольку оно не инициализировано. В стандарте также указано, что для некоторых объектов, которые по умолчанию инициализируются, означает, что "не инициализировано". Некоторые могут предположить, что это означает, что они неинициализированы, но это не указано, и я просто считаю, что такая производительность не требуется.

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

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

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

Итак, почему (необязательное) присутствие UB, похоже, оптимизирует оптимизацию, когда UB обычно воспринимается как возможность для большей оптимизации?

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


С++ 14 обновляет поведение, так что сам доступ к значениям неопределенных значений создает поведение undefined, независимо от того, считается ли данная переменная неинициализированной или нет: fooobar.com/questions/4500/...

Ответ 4

Это действительно очень интересно. Я пробовал ваш пример с MSVC 2013. Моя первая идея заключалась в том, что тот факт, что ++ a [j] несколько undefined, является причиной того, что цикл не удаляется, поскольку удаление этого будет определенно изменять значение программы из undefined/​​неправильной семантики в что-то значимое, поэтому я попытался инициализировать значения раньше, но петли все еще не исчезают.

Затем я заменил ++ a [j]; с a [j] = 0; который затем генерировал выход без какой-либо петли, поэтому все, что было между двумя вызовами clock(), было удалено. Я могу только догадываться о причине. Возможно, оптимизатор не может доказать, что оператор ++ не имеет побочных эффектов по какой-либо причине.