Оптимизация a "while(1);" в C++0x
обновление, см. ниже!
Я слышал и читал, что C++0x позволяет компилятору печатать "Hello" для следующего фрагмента
#include <iostream>
int main() {
while(1)
;
std::cout << "Hello" << std::endl;
}
это, по-видимому, имеет какое-то отношение к потокам и возможностям оптимизации. Мне кажется, что это может удивить многих людей.
есть ли у кого-то хорошее объяснение, почему это было необходимо разрешить? Для справки, самый последний проект C++0x говорит в 6.5/5
цикл, который, вне оператора for-init-statement в случае оператора for,
- не вызывает библиотечные функции ввода-вывода и
- не имеет доступа или изменения летучих объектов, и
- не выполняет никаких операций синхронизации (1.10) или атомарных операций (пункт 29)
можно предположить, что реализация завершится. [ Примечание: это предназначено для разрешения компилятора transfor-
маты, такие как удаление пустые петли, даже когда окончание не может быть доказано. - конец Примечание ]
Edit:
эта поучительная статья говорит о том, что текст стандартов
к сожалению, слова "неопределенное поведение" не используются. Однако всякий раз, когда стандарт говорит "компилятор может принять P", подразумевается, что программа, которая имеет свойство not-P, имеет неопределенную семантику.
это правильно, и это компилятор разрешил печатать " Bye " для вышеуказанной программы?
есть еще более глубокий здесь нити, что касается аналогичного изменения на C, начатого парнем, сделавшим вышеуказанную связанную статью. Среди других полезных фактов они представляют решение, которое, по-видимому, также применимо к C++0x (обновление: это не будет работать с n3225 - см. ниже!)
endless:
goto endless;
компилятор не может оптимизировать это, кажется, потому что это не петля, а прыжок. Другой парень суммирует предлагаемые изменения в C++0x и C201X
написав цикл, программист утверждает или что
цикл делает что-то с видимым поведением (выполняет ввод - вывод, обращается
летучие объекты, или выполняет синхронизацию или атомарные операции),
или что он в конечном итоге заканчивается. Если я нарушу это предположение
написав бесконечный цикл без побочных эффектов, я лгу
компилятор, и поведение моей программы не определено. (Если мне повезет,
компилятор может предупредить меня об этом.) Язык не предоставляет
(больше не дает?) способ выразить бесконечный цикл без
видимое поведение.
обновление на 3.1.2011 с n3225: комитет переместил текст в 1.10 / 24 и сказал
реализация может предполагать, что любой поток в конечном итоге сделает одно из следующих действий:
- расторгнут,
- вызовите библиотечную функцию ввода-вывода,
- доступ или изменение изменчивого объекта, или
- выполните операцию синхронизации или атомарную операцию.
The goto фокус не больше работать!
8 ответов:
есть ли у кого-то хорошее объяснение, почему это было необходимо разрешить?
да, Ганс Бем дает обоснование для этого в N1528: почему неопределенное поведение для бесконечных циклов?, хотя это документ WG14, обоснование применяется и к C++, и документ относится как к WG14, так и к WG21:
Как правильно указывает N1509, текущий проект по существу дает неопределенное поведение для бесконечных циклов в 6.8. 5p6. Главная проблема для это означает, что он позволяет коду перемещаться по потенциально безконечная петля. Например, предположим, что у нас есть следующие циклы, где count и count2 являются глобальными переменными (или имеют свой адрес taken), а 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. Таким образом он может выполняться параллельно с другим потоком, который обращается или счетчик2 обновления. Это больше не безопасно с преобразованной версией что делает доступ к count2, несмотря на бесконечный цикл. Таким образом преобразование потенциально вводит гонку данных.
в таких случаях очень маловероятно, что компилятор сможет подтверждать завершение цикла; он должен был бы понять, что Q точек к ациклическому списку, который, я считаю, находится за пределами возможностей большинства основные компиляторы, и часто невозможно без всей программы информация.
ограничения, налагаемые неперерывными циклами, являются ограничением на оптимизация завершающих циклов, для которых компилятор не может докажите прекращение, а также об оптимизации фактически незаконченные петли. Первые встречаются гораздо чаще, чем этот последнее, а зачастую и более интересное для оптимизации.
ясно также для-петли с целочисленной переменной цикла в что было бы трудно для компилятора доказать завершение, и таким образом, это будет сложно для компилятора, чтобы реструктуризировать петли без 6.8.5p6. Даже что-то вроде
for (i = 1; i != 15; i += 2)или
for (i = 1; i <= 10; i += j)кажется нетривиальной обработки. (В первом случае, некоторое базовое число теория необходима для того чтобы доказать прекращение, внутри последний случай нам нужен чтобы узнать что-то о возможных значениях j, чтобы сделать это. Оберните вокруг для целых чисел без знака может усложнить некоторые из этих рассуждений дальше.)
эта проблема, похоже, относится почти ко всем циклам реструктуризации преобразования, включая распараллеливание компилятора и преобразования оптимизации кэша, оба из которых, вероятно, получат по важности,и уже часто важны для числового кода. Это, по-видимому, превратится в существенный стоимость в интересах возможность писать бесконечные циклы самым естественным образом, тем более что большинство из нас редко пишут намеренно бесконечные циклы.
одно главное отличие от C заключается в том, что C11 предоставляет исключение для управления выражениями, которые являются постоянными выражениями который отличается от C++ и делает ваш конкретный пример хорошо определенным в C11.
для меня соответствующее оправдание:
это предназначено для разрешения преобразований компилятора, таких как удаление пустых циклов, даже если завершение не может быть доказано.
предположительно, это потому, что доказательство прекращения механическисложно, и невозможность доказать завершение затрудняет компиляторы, которые в противном случае могли бы сделать полезные преобразования, такие как перемещение независящих операций из цикла до цикла после или наоборот, выполнение операций после цикла в одном потоке, в то время как цикл выполняется в другом, и так далее. Без этих преобразований цикл может блокировать все другие потоки, пока они ждут, пока один поток завершит указанный цикл. (Я использую" поток " свободно, чтобы означать любую форму параллельной обработки, включая отдельные потоки команд VLIW.)
редактировать: тупой пример:
while (complicated_condition()) { x = complicated_but_externally_invisible_operation(x); } complex_io_operation(); cout << "Results:" << endl; cout << x << endl;здесь, это было бы быстрее для одного потока, чтобы сделать
complex_io_operationв то время как другой делает все сложные вычисления в цикле. Но без предложения, которое вы процитировали, компилятор должен доказать две вещи, прежде чем он сможет выполнить оптимизацию: 1) thatcomplex_io_operation()не зависит от результатов цикла, и 2) что цикл завершится. Доказательство 1) довольно легко, доказательство 2) является проблемой остановки. С предложением он может предположить, что цикл завершается и получает выигрыш от распараллеливания.Я также предполагаю, что дизайнеры считали, что случаи, когда бесконечны циклы происходят в производственном коде очень редко и обычно являются такими вещами, как управляемые событиями циклы, которые каким-то образом обращаются к вводу-выводу. В результате они пессимизировали редкий случай (бесконечные петли) в пользу оптимизации более общего случая (неинфинированные, но трудно механически доказать неинфинированные петли).
это, однако, означает, что бесконечные циклы, используемые в обучающих примерах, будут страдать в результате и будут вызывать gotchas в начальном коде. Я не могу сказать, что это полностью хорошо вещь.
EDIT: что касается проницательной статьи, которую вы сейчас ссылаетесь, я бы сказал, что "компилятор может предположить X о программе" логически эквивалентно "если программа не удовлетворяет X, поведение не определено". Мы можем показать это следующим образом: предположим, что существует программа, которая не удовлетворяет свойству X. Где бы определялось поведение этой программы? Стандарт определяет поведение только при условии, что свойство X истинно. Хотя в стандарте это явно не указано объявите поведение неопределенным, оно объявило его неопределенным опущением.
рассмотрим аналогичный аргумент:" компилятор может предположить, что переменная x назначается только не более одного раза между точками последовательности "эквивалентно"назначение x более одного раза между точками последовательности не определено".
Я думаю, что правильная интерпретация-это одна из ваших правок: пустые бесконечные циклы-это неопределенное поведение.
Я бы не сказал, что это особенно интуитивное поведение, но эта интерпретация имеет больше смысла, чем альтернативная, что компилятору произвольно разрешено игнорировать бесконечные циклы без вызова UB.
если бесконечные циклы UB, это просто означает, что не завершающие программы не считаются значимыми: согласно C++0x, они есть нет семантики.
Это тоже имеет определенный смысл. Это особый случай, когда ряд побочных эффектов просто больше не возникает (например, ничего не возвращается из
main), и ряд оптимизаций компилятора затрудняется необходимостью сохранения бесконечных циклов. Например, перемещение вычислений по циклу совершенно допустимо, если цикл не имеет побочных эффектов, потому что в конечном итоге вычисление будет выполнено в любом случае. Но если цикл никогда не завершается, мы не можем безопасно переставить код через него, потому что мы может просто измените, какие операции на самом деле выполняются до зависания программы. Если мы не рассматриваем висячую программу как UB, то есть.
соответствующая проблема заключается в том, что компилятору разрешено переупорядочивать код, побочные эффекты которого не конфликтуют. Неожиданный порядок выполнения может возникнуть даже в том случае, если компилятор выдаст нескончаемый машинный код для бесконечного цикла.
Я считаю, что это правильный подход. Спецификация языка определяет способы обеспечения порядка выполнения. Если вы хотите бесконечный цикл, который не может быть переупорядочен, напишите следующее:
volatile int dummy_side_effect; while (1) { dummy_side_effect = 0; } printf("Never prints.\n");
Я думаю, что этот вопрос, возможно, лучше всего сформулировать так: "если более поздний фрагмент кода не зависит от более раннего фрагмента кода, и более ранний фрагмент кода не имеет побочных эффектов ни на какой другой части системы, выходные данные компилятора могут выполнять более поздний фрагмент кода до, после или смешанный с выполнением первого, даже если первый содержит циклы,без учета того, когда и будет ли фактически завершен предыдущий код. Например, компилятор может перепишите:
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++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++}; } printf("The result is "); 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++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++}; } signal_other_thread_and_die(); } else // Second thread { printf("The result is "); wait_for_other_thread(); } printf("%d/%d/%d", a,b,c); }как правило, неразумно, хотя я мог бы беспокоиться, что:
int total=0; for (i=0; num_reps > i; i++) { update_progress_bar(i); total+=do_something_slow_with_no_side_effects(i); } show_result(total);станет
int total=0; if (fork_is_first_thread()) { for (i=0; num_reps > i; i++) total+=do_something_slow_with_no_side_effects(i); signal_other_thread_and_die(); } else { for (i=0; num_reps > i; i++) update_progress_bar(i); wait_for_other_thread(); } show_result(total);имея один процессор обрабатывать вычисления, а другой обрабатывать обновления индикатора выполнения, переписать будет повысить эффективность. К сожалению, это сделало бы обновления индикатора выполнения менее полезными, чем они должны быть.
Это не разрешимо для компилятора для нетривиальных случаев, если это вообще бесконечный цикл.
в разных случаях может случиться так, что ваш оптимизатор достигнет лучшего класса сложности для вашего кода (например, это было O(n^2), и вы получите O(n) или O(1) после оптимизации).
таким образом, включение такого правила, которое запрещает удаление бесконечного цикла в стандарт C++, сделало бы невозможным многие оптимизации. И большинство людей этого не хотят. Я думаю, что это довольно ответить ваш вопрос.
другое дело: я никогда не видел ни одного действительного примера, где вам нужен бесконечный цикл, который ничего не делает.
один пример, о котором я слышал, был уродливым взломом, который действительно должен быть решен иначе: речь шла о встроенных системах, где единственным способом вызвать Сброс было заморозить устройство, чтобы сторожевой пес перезапустил его автоматически.
Если вы знаете какой-либо действительный / хороший пример, где вам нужен бесконечный цикл, который делает ничего, пожалуйста, скажи мне.
Я думаю, стоит отметить, что циклы, которые были бы бесконечными, за исключением того, что они взаимодействуют с другими потоками через энергонезависимые, несинхронизированные переменные, теперь могут привести к неправильному поведению с новым компилятором.
другими словами, сделайте ваши глобалы изменчивыми - а также аргументы, переданные в такой цикл через указатель / ссылку.
Comments