g ++ — константа C ++, складывающая цикл для простых чисел

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

#include <iostream>

using namespace std;

inline bool is_prime(int n)
{
if(n<2)
return false;
for(int i=2;i*i<=n;i++)
if(n%i==0)
return false;
return true;
}

int main()
{
for(int i=0;i<20;i++)
if(is_prime(i))
cout<<i<<endl;
return 0;
}

И я строю это через:

g++ -O3 -S main.cpp -o main.asm

В результате несколько:

2,3,5,7,11,13,17,19

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

for(int x:{2,3,5,7,11,13,17,19})
cout<<x<<endl;

или же

cout<<  2 <<endl;
cout<<  3 <<endl;
cout<<  5 <<endl;
cout<<  7 <<endl;
cout<< 11 <<endl;
cout<< 13 <<endl;
cout<< 17 <<endl;
cout<< 19 <<endl;

Но чтение сборки показывает, что ничего не происходит.

Я даже использовал __builtin_expect но это не сработало.

Есть ли способ заставить оптимизатор компилятора читать цикл for и использовать преимущество в том, что выходные данные известны?

Я хотел бы сделать это без использования шаблонного метапрограммирования.

PS. Моя настоящая цель — просто протестировать компилятор, а не эффективный метод вычисления простых чисел. Я просто хочу показать своим друзьям, насколько мощным является компилятор C ++.


Если разделение is_prime Это вызывает беспокойство, я положил все внутри основного и никакой разницы не наблюдалось:

#include <iostream>

using namespace std;

int main()
{
for(int n=2;n<20;n++)
{
bool is_prime=true;
for(int i=2;i*i<=n;i++)
if(n%i==0)
{
is_prime=false;
break;
}
if(is_prime)
cout<<n<<endl;
}
return 0;
}

Есть еще один пример, который не оправдывает компилятор:

#include <iostream>
#include <vector>

using namespace std;

int prime_after6000()
{
int n=6000;
do
{
bool is_prime=true;
for(int i=2;i*i<=n;i++)
if(n%i==0)
{
is_prime=false;
break;
}
if(is_prime)
return n;
n++;
}while(true);
}

int main()
{
cout<<prime_after6000()<<endl;
return 0;
}

монтаж:

...
main:
.LFB1907:
.cfi_startproc
subq    $8, %rsp
.cfi_def_cfa_offset 16
movl    $6000, %esi   ;;;;;;;;;;;;;;;;;;;; bad
.L18:
testb   $1, %sil
je  .L15
movl    $2, %ecx
jmp .L16
.p2align 4,,10
.p2align 3
.L17:
movl    %esi, %eax
cltd
idivl   %ecx
testl   %edx, %edx
je  .L15
.L16:
addl    $1, %ecx
movl    %ecx, %eax
imull   %ecx, %eax
cmpl    %esi, %eax
jle .L17
movl    $_ZSt4cout, %edi
call    _ZNSolsEi
movq    %rax, %rdi
call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
xorl    %eax, %eax
addq    $8, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.p2align 4,,10
.p2align 3
.L15:
.cfi_restore_state
addl    $1, %esi
jmp .L18
.cfi_endproc
.LFE1907:
.size   main, .-main
.p2align 4,,15
.type   _GLOBAL__sub_I__Z15prime_after6000v, @function
_GLOBAL__sub_I__Z15prime_after6000v:
...

10

Решение

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

Основной характеристикой программы является то, что она не принимает никаких входных данных, но выдает выходные данные, записывая в cout, Имейте в виду, что is_prime функция не встроенный компилятор; компилятор рассматривает это как просто еще одну функцию. Это важно, и я вернусь к этому позже.

Как компилятор преобразует программу так, как вы описали? Как это может сделать что-то подобное? То есть, как компилятор может преобразовать эти два вложенных цикла в простую последовательность операторов, которые записывают целые числа в cout? Единственный способ сделать это — проведение программа, чтобы выяснить все значения, которые должны быть записаны в cout,

Это не имеет никакого смысла, не так ли? Давайте посмотрим на общую картину и рассмотрим все программы (или последовательности операторов), которые имеют одинаковую характеристику; те, которые не принимают никакого ввода, но испускают вывод. Возникает вопрос: почему компилятор не выполняет исходный код и не просто генерирует код, записывающий выходные значения? По следующим причинам:

  • Что если выполнение программы занимает слишком много времени? Что, если в нем была ошибка, которая заставляла его работать в течение неожиданного количества времени? Нет даже гарантии, что программа когда-нибудь остановится. Что именно должен делать компилятор?
  • В конечном счете, цель компилятора не в том, чтобы выполнить исходный код, а в том, чтобы создать функционально эквивалентный нативный код, который потенциально оптимизирован. В конце концов, если программа не принимает никаких входных данных (например, ваш код), вы можете просто скомпилировать код и запустить его один раз, чтобы увидеть выходные данные. Код должен быть выполнен в любом случае, компилятором или запуском исполняемого двоичного файла, и в любом случае это займет одинаковое количество времени. В обоих случаях код должен быть скомпилирован и выполнен. Так что такая оптимизация не добавляет реальной ценности. Тем не менее, это в отличие от шаблонов, которые предназначена быть сведенным к обычному коду компилятором во время компиляции. Кроме того, интерпретация была бы лучшей моделью выполнения для таких программ. Вы не хотите беспокоиться о компиляции кода? Идите вперед и используйте интерпретатор Python или любой другой интерпретатор.
  • Реализация такой оптимизации может быть трудной или невозможной в целом. Что если механизм, использованный для создания выходных данных, имеет побочные эффекты, которые могут изменить будущие выходные значения? Компилятор точно не знает, что происходит, когда вы пишете cout, Стандартный поток вывода может быть перенаправлен на что-то странное. Таким образом, программы, которые не принимают никакого ввода, не обязательно проще для компилятора оптимизировать.

Тем не менее, простые куски кода, которые могут быть оценены за очень ограниченное количество времени, действительно оцениваются компилятором. Эта оптимизация называется постоянное складывание. Куски кода, которые не влияют на состояние программы, могут быть удалены без их выполнения. Например, если вы удалили cout<<i<<endl;компилятор просто оптимизирует оставшуюся часть кода. Это называется устранение мертвого кода. Компиляторы выполняют эти оптимизации, потому что они могут выполняться компилятором систематическим образом и потому, что они очень эффективны на реальных кодовых базах.

Но что произойдет, если is_prime функция была присуща компилятору? В этом случае компилятор, вероятно, будет иметь встроенную таблицу часто используемых простых чисел и очень быструю реализацию тестирования простоты. Затем вы можете ожидать от компилятора развернуть loop в основной функции несколько раз, может быть, даже полностью, содержащий только выходные операторы, по существу выполняя преобразование, которое вы ищете.

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]