Может ли компилятор C ++ оптимизировать код при работе с указателями? - PullRequest
3 голосов
/ 01 сентября 2010

С этими двумя вопросами в качестве фона ( first и second ) мне стало интересно, сколько оптимизации может выполнять компилятор C ++ при работе с указателями?В частности, меня интересует, насколько умным является компилятор, когда он оптимизирует код, который он может обнаружить, никогда не будет запущен.

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

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


void test() {
    bool escape = false;

    while ( !escape ); // Will never be exited

    // Do something useful after having escaped
}

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

Теперь, если бы мы изменили переменную на указатель, компилятор все равно оптимизировал бы цикл?Скажем, код выглядит так:


void test( bool* escape ) {
    while ( *escape ); // Will this be executed?

    // Do something useful after having escaped
}

Я подозреваю, что компилятор покончит с циклом, иначе ключевое слово volatile будет избыточным, да?Но как насчет того, чтобы при работе с потоками - где это на самом деле изменено, но вне функции, и, возможно, даже вне этого файла C ++ - компилятор все равно удалит цикл?Имеет ли значение, если переменная, на которую указывает escape, является глобальной переменной или локальной переменной внутри другой функции?Может ли компилятор сделать это обнаружение?В этом вопросе некоторые говорят, что компилятор не будет оптимизировать цикл, если внутри цикла вызываются библиотечные функции.Какие механизмы тогда используются при использовании библиотечных функций, которые предотвращают эту оптимизацию?

Ответы [ 5 ]

9 голосов
/ 01 сентября 2010

В первом случае (while ( !escape );) компилятор обработает это как label: goto label; и пропустит все после него (и, вероятно, выдаст вам предупреждение).

Во втором случае (while ( *escape );) компилятор не может знать, будет ли * escape истинным или ложным при запуске, поэтому он должен выполнять comaprision и loop или нет. Однако обратите внимание, что он должен прочитать значение из * escape только один раз, то есть он может обработать это как:

 bool b = *escape;
 label:   if (b) goto label;

volatile заставит его читать значение из 'escape' каждый раз через цикл.

2 голосов
/ 01 сентября 2010

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

Стандарт охватывает это в «1.9 Выполнение программы». Стандарт описывает своего рода абстрактную машину, и реализация должна придумать такое же «наблюдаемое поведение». (Это правило «как будто», как указано в сноске.)

Начиная с 1.9 (6): «Наблюдаемое поведение абстрактной машины - это последовательность операций чтения и записи в volatile данных и вызовов функций ввода-вывода библиотеки». Это означает, что, если вы можете показать, что изменение функции не приведет ни к изменению ни в этой функции, ни после ее вызова, изменение является допустимым.

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

2 голосов
/ 01 сентября 2010

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

Но, конечно, вы склоняетесь к поиску использования для ключевого слова volatile . Вы можете найти много тем на SO, говорящих о том, почему volatile не подходит для многопоточного приложения.

2 голосов
/ 01 сентября 2010

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

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

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

1 голос
/ 02 сентября 2010

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

Я видел компилятор, который оптимизировал множество строк кода в разных функциях, вызываемых в цикле. На самом деле я пытался провести сравнение компилятора с помощью рандомизатора lfsr, который вызывался неоднократно в цикле (цикл запускался жестко заданным числом раз). Один компилятор добросовестно компилировал (и оптимизировал) код, выполняя каждый функциональный шаг, другой компилятор выяснил, что я делал, и созданный ассемблер был эквивалентен ldr r0, # 0x12345, где 0x12345 был ответом, если вы вычислили все переменные как сколько раз цикл был запущен. Одна инструкция.

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

  ldr r0,[r1]
compare:
  cmp r0,#0
  bne compare

Именно урок, который я усвоил, когда узнал о летучем. Место в памяти было прочитано (один раз), а затем цикл ждал, пока это значение изменится, так же, как мой код «сказал» это сделать. Не то, что я «хотел», чтобы это делало (выход из цикла, когда регистр, на который указывает указатель, изменился).

Теперь, если бы вы сделали что-то вроде этого:

void test( bool* escape ) {
    while ( *escape );
}

pretest() {
    bool escape = false;
    test(&escape);
}

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

Суть в том, что ваши два примера совершенно разные, в первом случае все, что нужно знать компилятору, чтобы определить, что цикл while является nop, - это все. Во втором случае компилятор не имеет возможности узнать состояние escape или, если он когда-либо изменится, и должен добросовестно скомпилировать цикл while в исполняемые инструкции. Единственная возможность оптимизации заключается в том, читает ли она из памяти при каждом проходе цикла.

Если вы хотите действительно понять, что происходит, скомпилируйте эти функции с различными вариантами оптимизации, а затем разберите объектный код.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...