Оптимизация «время (1);» в C ++ 0x


153

Обновлено, смотрите ниже!

Я слышал и читал, что C ++ 0x позволяет компилятору напечатать «Hello» для следующего фрагмента

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}

По-видимому, это как-то связано с потоками и возможностями оптимизации. Мне кажется, что это может удивить многих людей, хотя.

У кого-нибудь есть хорошее объяснение того, почему это необходимо было разрешить? Для справки, самый последний проект C ++ 0x гласит:6.5/5

Цикл, который вне оператора for-init-в случае оператора for,

  • не вызывает функции библиотеки ввода-вывода, и
  • не получает доступ и не изменяет изменчивые объекты, и
  • не выполняет операции синхронизации (1.10) или атомарные операции (пункт 29)

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

Редактировать:

Эта проницательная статья говорит о том, что текст стандартов

К сожалению, слова «неопределенное поведение» не используются. Однако всякий раз, когда в стандарте говорится, что «компилятор может принять P», подразумевается, что программа со свойством not-P имеет неопределенную семантику.

Это правильно, и разрешено ли компилятору печатать «пока» для вышеуказанной программы?


Здесь есть еще более проницательная тема , посвященная аналогичному изменению C, начатая парнем из вышеупомянутой связанной статьи. Среди других полезных фактов они представляют решение, которое, похоже, также применимо к C ++ 0x ( обновление : это больше не будет работать с n3225 - см. Ниже!)

endless:
  goto endless;

Кажется, компилятору не разрешено оптимизировать это, потому что это не цикл, а скачок. Другой парень обобщает предложенное изменение в C ++ 0x и C201X

При написании цикла, программист утверждать , либо , что петля делает что - то с видимым поведением (выполняют I / O, получает доступ летучих объектов, или выполняет синхронизацию или атомарные операции), или , что он в конечном итоге заканчивается. Если я нарушу это предположение, написав бесконечный цикл без побочных эффектов, я лгу компилятору, и поведение моей программы не определено. (Если мне повезет, компилятор может предупредить меня об этом.) Язык не предоставляет (больше не предоставляет?) Способ выразить бесконечный цикл без видимого поведения.


Обновление 3.1.2011 с n3225: Комитет переместил текст в 1.10 / 24 и произнес

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

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

gotoТрюк будет не работать больше!


4
while(1) { MyMysteriousFunction(); }должен быть независимо компилируемым, не зная определения этой загадочной функции, верно? Итак, как мы можем определить, выполняет ли он какие-либо функции библиотеки ввода-вывода? Другими словами: конечно, первая фраза, которую можно сформулировать, не вызывает функций .
Даниэль Эрвикер

19
@Daniel: если у него есть доступ к определению функции, он может многое доказать. Существует такая вещь, как межпроцедурная оптимизация.
Potatoswatter

3
Прямо сейчас, в C ++ 03, разрешено ли менять компилятор int x = 1; for(int i = 0; i < 10; ++i) do_something(&i); x++;на for(int i = 0; i < 10; ++i) do_something(&i); int x = 2;? Или, возможно, другой путь, с xинициализацией 2до цикла. Он может сказать, do_somethingчто не заботится о значении x, поэтому это совершенно безопасная оптимизация, если do_something она не приводит iк изменению значения так, что вы попадаете в бесконечный цикл.
Деннис Зикефуз

4
Так значит ли это, что он main() { start_daemon_thread(); while(1) { sleep(1000); } }может просто завершиться немедленно вместо запуска моего демона в фоновом потоке?
Гейб

2
«Эта проницательная статья» предполагает, что конкретное поведение является неопределенным поведением только потому, что не существует явного, определенного поведения. Это неверное предположение. В общем, когда стандарт оставляет открытым конечное число поведений, реализация должна выбрать любой из них ( неуказанное поведение). Это не должно быть детерминированным. Завершение ли цикла «ничего не делать», возможно, является логическим выбором; либо это так, либо нет. Делать что-то еще не разрешено.
MSalters

Ответы:


33

У кого-нибудь есть хорошее объяснение того, почему это необходимо было разрешить?

Да, Ганс Бём приводит обоснование этого в N1528: почему неопределенное поведение для бесконечных циклов? , хотя это документ WG14, обоснование применимо и к C ++, а документ относится как к WG14, так и к WG21:

Как правильно указывает N1509, текущий черновик по существу дает неопределенное поведение бесконечным циклам в 6.8.5p6. Основной проблемой для этого является то, что он позволяет коду перемещаться по потенциально не завершающемуся циклу. Например, предположим, что у нас есть следующие циклы, где count и count2 являются глобальными переменными (или у них был взят их адрес), а p является локальной переменной, адрес которой не был взят:

for (p = q; p != 0; p = p -> next) {
    ++count;
}
for (p = q; p != 0; p = p -> next) {
    ++count2;
}

Могут ли эти два цикла быть объединены и заменены следующим циклом?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

Без специального разрешения в 6.8.5p6 для бесконечных циклов это было бы запрещено: если первый цикл не заканчивается, поскольку q указывает на циклический список, оригинал никогда не записывает в count2. Таким образом, он может быть запущен параллельно с другим потоком, который обращается или обновляет count2. Это больше не безопасно с преобразованной версией, которая делает доступ к count2, несмотря на бесконечный цикл. Таким образом, преобразование потенциально вводит гонку данных.

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

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

Очевидно, существуют также циклы for с целочисленной переменной цикла, в которых компилятору будет трудно доказать завершение, и, таким образом, компилятору будет трудно реструктурировать циклы без 6.8.5p6. Даже что-то вроде

for (i = 1; i != 15; i += 2)

или

for (i = 1; i <= 10; i += j)

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

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

Единственное существенное отличие от C состоит в том, что C11 предоставляет исключение для управления выражениями, которые являются константными выражениями, что отличается от C ++ и делает ваш конкретный пример хорошо определенным в C11.


1
Существуют ли какие-либо безопасные и полезные оптимизации, которые облегчаются существующим языком, которые не могут быть упрощены так же хорошо, говоря: «Если завершение цикла зависит от состояния каких-либо объектов, время, необходимое для выполнения цикла, не считается наблюдаемый побочный эффект, даже если такое время оказывается бесконечным ». Учитывая, что использование do { x = slowFunctionWithNoSideEffects(x);} while(x != 23);кода после цикла, от которого он не будет зависеть, xможет показаться безопасным и разумным, но допущение компилятора x==23в таком коде кажется более опасным, чем полезным.
суперкат

47

Для меня соответствующим обоснованием является:

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

Предположительно, это связано с тем, что механическое доказательство завершения затруднено , а невозможность доказать завершение мешает компиляторам, которые в противном случае могли бы выполнять полезные преобразования, такие как перемещение независимых операций из цикла перед циклом в цикл после и наоборот, выполнение операций после цикла в одном потоке, пока цикл выполняется в другом, и так далее. Без этих преобразований цикл может блокировать все другие потоки, пока они ожидают, пока один поток завершит указанный цикл. (Я использую «поток» свободно для обозначения любой формы параллельной обработки, включая отдельные потоки команд VLIW.)

РЕДАКТИРОВАТЬ: тупой пример:

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;

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

Я также представляю, что разработчики считали, что случаи, когда бесконечные циклы происходят в производственном коде, очень редки и обычно представляют собой такие вещи, как управляемые событиями циклы, которые каким-то образом обращаются к вводу / выводу. В результате они пессимизировали редкий случай (бесконечные циклы) в пользу оптимизации более распространенного случая (бесконечный, но трудно механически доказать бесконечный цикл).

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

РЕДАКТИРОВАТЬ: что касается проницательной статьи, которую вы сейчас связываете, я бы сказал, что «компилятор может предположить, что X о программе» логически эквивалентно «если программа не удовлетворяет X, поведение не определено». Мы можем показать это следующим образом: предположим, что существует программа, которая не удовлетворяет свойству X. Где будет определено поведение этой программы? Стандарт определяет поведение только при условии, что свойство X истинно. Хотя Стандарт не объявляет явное поведение как неопределенное, он объявляет его неопределенным из-за пропуска.

Рассмотрим аналогичный аргумент: «компилятор может предположить, что переменная x назначается не более одного раза между точками последовательности», что эквивалентно «назначение x более одного раза между точками последовательности не определено».


«Доказательство 1) довольно просто» - на самом деле, не следует ли сразу из 3 условий, чтобы компилятор мог допустить завершение цикла в соответствии с предложением, о котором спрашивает Йоханнес? Я думаю, что они сводятся к тому, что «цикл не имеет наблюдаемого эффекта, за исключением, возможно, вращения навсегда», и предложение гарантирует, что «вращение навсегда» не гарантирует поведения для таких циклов.
Стив Джессоп

@Steve: легко, если цикл не завершается; но если цикл завершается, он может иметь нетривиальное поведение, которое влияет на обработку complex_io_operation.
Филипп Поттер

Ой, да, я упустил, что это может изменить энергонезависимые локальные / псевдонимы / все, что используется в операциях ввода-вывода. Таким образом, вы правы: хотя это не обязательно следует, есть много случаев, когда компиляторы могут и доказать, что такая модификация не происходит.
Стив Джессоп

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

1
@supercat: То, что вы описываете, это то, что будет происходить на практике, но это не то, что требует проект стандарта. Мы не можем предполагать, что компилятор не знает, завершится ли цикл. Если компилятор делает знает цикл не закончится, он может делать все , что угодно. DS9K будет создавать носовые демонов для любого бесконечного цикла, без ввода / вывода и т.д. (Поэтому DS9K решает проблемы остановки.)
Philip Поттер

15

Я думаю, что правильная интерпретация - это то, что вы написали: пустые бесконечные циклы - неопределенное поведение.

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

Если бесконечные циклы - это UB, это просто означает, что не завершающиеся программы не считаются значимыми: согласно C ++ 0x они не имеют семантики.

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


7
«Пустые бесконечные циклы - неопределенное поведение»? Алан Тьюринг стал бы умолять, но только тогда, когда он перевернется в своей могиле.
Donal Fellows

11
@Donal: Я никогда не говорил ничего о его семантике в машине Тьюринга. Мы обсуждаем семантику бесконечного цикла без побочных эффектов в C ++ . И когда я это прочитал, C ++ 0x решил сказать, что такие циклы не определены.
2010 года

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

1
Значит ли это, что C ++ 0x не подходит для встроенных устройств? Почти все встраиваемые устройства не являются оконечными и выполняют свою работу в большом объеме while(1){...}. Они даже обычно используют, while(1);чтобы вызвать сброс с помощью сторожевого таймера.
вс

1
@vsz: первая форма в порядке. Бесконечные циклы прекрасно определены, если они имеют некоторое наблюдаемое поведение. Вторая форма сложнее, но я могу придумать два очень простых выхода: (1) компилятор, нацеленный на встроенные устройства, может просто выбрать более жесткое поведение в этом случае, или (2) вы создаете тело, которое вызывает некоторую фиктивную библиотечную функцию , Пока компилятор не знает, что делает эта функция, он должен предполагать, что у нее может быть какой-то побочный эффект, и поэтому он не может связываться с циклом.
Джалф

8

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


3
Хороший вопрос Похоже, у этого парня была именно та проблема, которую этот параграф позволяет этому компилятору вызвать В связанном обсуждении одним из ответов написано, что «к сожалению, слова« неопределенное поведение »не используются. Однако всякий раз, когда стандарт говорит« компилятор может принять P », подразумевается, что программа, которая имеет свойство not-P имеет неопределенную семантику. " , Это удивляет меня. Означает ли это, что мой пример программы, приведенной выше, имеет неопределенное поведение, и может просто вызвать ошибку из ниоткуда?
Йоханнес Шауб -

@Johannes: текст «может быть предположен» не встречается нигде в проекте, который я должен передать, и «может предполагать» встречается только пару раз. Хотя я проверил это с помощью функции поиска, которая не совпадает с разрывами строк, поэтому я, возможно, пропустил некоторые из них. Так что я не уверен, что обобщение автора оправдано на доказательствах, но как математик я должен признать логику аргумента, что если компилятор допускает что-то ложное, то в целом он может вывести что-нибудь ...
Стив Джессоп

... Разрешение противоречия в рассуждениях компилятора о программе, безусловно, очень сильно намекает на UB, поскольку, в частности, оно позволяет компилятору для любого X сделать вывод, что программа эквивалентна X. Конечно, разрешив компилятору сделать вывод, что позволяя это сделать это. Я также согласен с автором в том, что, если UB предназначен, он должен быть явно указан, а если он не предназначен, то текст спецификации является неправильным и должен быть исправлен (возможно, эквивалентом языка спецификации, "компилятор может заменить цикл с кодом, который не имеет никакого эффекта ", я не уверен).
Стив Джессоп

@SteveJessop: Что бы вы подумали о том, чтобы просто сказать, что выполнение любого фрагмента кода, включая бесконечные циклы, может быть отложено до тех пор, пока фрагмент кода не повлияет на наблюдаемое поведение программы, и для этого Как правило, время, необходимое для выполнения фрагмента кода, даже если оно бесконечно, не является "наблюдаемым побочным эффектом". Если компилятор может продемонстрировать, что цикл не может завершиться без переменной, содержащей определенное значение, можно считать, что переменная содержит это значение, даже если можно также показать, что цикл не может завершиться, если он содержит это значение.
суперкат

@supercat: как вы уже сказали, я не думаю, что это улучшит ситуацию. Если цикл доказуемо никогда не завершается, то для любого объекта Xи битового шаблона xкомпилятор может продемонстрировать, что цикл не завершается без Xудержания битового шаблона x. Это пустая правда. Таким образом, Xможно считать, что он содержит любой битовый паттерн, и это так же плохо, как UB, в том смысле, что он неправильный, Xи xон быстро вызовет некоторые. Поэтому я считаю, что вам нужно быть более точным в своем стандарте. Трудно говорить о том, что происходит «в конце» бесконечного цикла, и показывать его эквивалентным некоторой конечной операции.
Стив Джессоп

8

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

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

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");

2
@ JohannesSchaub-litb: Если цикл - бесконечный или нет - не читает и не записывает какие-либо переменные переменные во время выполнения и не вызывает никаких функций, которые могут это сделать, компилятор может отложить любую часть цикла до первая попытка получить доступ к чему-то вычисленному в нем. Учитывая unsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy);, что первое fprintfможет выполняться, а второе - нет (компилятор может перемещать вычисления dummyмежду двумя fprintf, но не дальше того, который печатает его значение).
суперкат

1

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

void testfermat (int n)
{
  int a = 1, b = 1, c = 1;
  while (pow (a, n) + pow (b, n)! = pow (c, n))
  {
    if (b> a) a ++; иначе, если (c> b) {a = 1; б ++}; еще {а = 1; B = 1; C ++};
  }
  printf («Результат есть»);
  printf («% d /% d /% d», a, b, c);
}

так как

void testfermat (int n)
{
  if (fork_is_first_thread ())
  {
    int a = 1, b = 1, c = 1;
    while (pow (a, n) + pow (b, n)! = pow (c, n))
    {
      if (b> a) a ++; иначе, если (c> b) {a = 1; б ++}; еще {а = 1; B = 1; C ++};
    }
    signal_other_thread_and_die ();
  }
  else // Второй поток
  {
    printf («Результат есть»);
    wait_for_other_thread ();
  }
  printf («% d /% d /% d», a, b, c);
}

Как правило, это не лишено смысла, хотя я могу беспокоиться, что:

  int total = 0;
  для (i = 0; num_reps> i; i ++)
  {
    update_progress_bar (я);
    всего + = do_something_slow_with_no_side_effects (I);
  }
  show_result (всего);

станет

  int total = 0;
  if (fork_is_first_thread ())
  {
    для (i = 0; num_reps> i; i ++)
      всего + = do_something_slow_with_no_side_effects (I);
    signal_other_thread_and_die ();
  }
  еще
  {
    для (i = 0; num_reps> i; i ++)
      update_progress_bar (я);
    wait_for_other_thread ();
  }
  show_result (всего);

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


Я думаю, что ваш индикатор выполнения не может быть разделен, потому что отображение индикатора выполнения - это вызов библиотеки ввода-вывода. Оптимизации не должны изменять видимое поведение таким образом.
Филипп Поттер

@Philip Potter: Если бы медленная рутина имела побочные эффекты, это, безусловно, было бы правдой. В моем предыдущем примере это было бы бессмысленно, если бы этого не произошло, поэтому я изменил это. Моя интерпретация спецификации заключается в том, что системе разрешено откладывать выполнение медленного кода до тех пор, пока его эффекты (кроме времени, которое требуется для выполнения) не станут видимыми, то есть вызов show_result (). Если индикатор выполнения использовал промежуточный итог или, по крайней мере, делал вид, что это делает, это заставит его синхронизироваться с медленным кодом.
суперкат

1
Это объясняет все те индикаторы выполнения, которые идут быстро от 0 до 100, а затем зависают целую вечность;)
Пол

0

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

В разных случаях может случиться, что ваш оптимизатор достигнет лучшего класса сложности для вашего кода (например, это был O (n ^ 2), и вы получите O (n) или O (1) после оптимизации).

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


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

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

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


1
Пример, где вам может понадобиться бесконечный цикл: встроенная система, в которой вы не хотите спать по соображениям производительности, и весь код зависает от одного или двух прерываний?
JCx

@JCx в стандарте C прерывания должны устанавливать флаг, который проверяет основной цикл, поэтому основной цикл будет иметь наблюдаемое поведение в случае установки флагов. Выполнение существенного кода в прерываниях непереносимо.
ММ

-1

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

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


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

1
Это ужасный совет. Делать их volatileне нужно и не достаточно, и это резко снижает производительность.
Дэвид Шварц
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.