Посмотрев на предыдущие вопросы 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:
...
Здесь есть фундаментальное недопонимание компиляторов. Давайте внимательно рассмотрим программу, которую вы написали, и подумаем о том, что вы ожидаете от компилятора.
Основной характеристикой программы является то, что она не принимает никаких входных данных, но выдает выходные данные, записывая в cout
, Имейте в виду, что is_prime
функция не встроенный компилятор; компилятор рассматривает это как просто еще одну функцию. Это важно, и я вернусь к этому позже.
Как компилятор преобразует программу так, как вы описали? Как это может сделать что-то подобное? То есть, как компилятор может преобразовать эти два вложенных цикла в простую последовательность операторов, которые записывают целые числа в cout
? Единственный способ сделать это — проведение программа, чтобы выяснить все значения, которые должны быть записаны в cout
,
Это не имеет никакого смысла, не так ли? Давайте посмотрим на общую картину и рассмотрим все программы (или последовательности операторов), которые имеют одинаковую характеристику; те, которые не принимают никакого ввода, но испускают вывод. Возникает вопрос: почему компилятор не выполняет исходный код и не просто генерирует код, записывающий выходные значения? По следующим причинам:
cout
, Стандартный поток вывода может быть перенаправлен на что-то странное. Таким образом, программы, которые не принимают никакого ввода, не обязательно проще для компилятора оптимизировать.Тем не менее, простые куски кода, которые могут быть оценены за очень ограниченное количество времени, действительно оцениваются компилятором. Эта оптимизация называется постоянное складывание. Куски кода, которые не влияют на состояние программы, могут быть удалены без их выполнения. Например, если вы удалили cout<<i<<endl;
компилятор просто оптимизирует оставшуюся часть кода. Это называется устранение мертвого кода. Компиляторы выполняют эти оптимизации, потому что они могут выполняться компилятором систематическим образом и потому, что они очень эффективны на реальных кодовых базах.
Но что произойдет, если is_prime
функция была присуща компилятору? В этом случае компилятор, вероятно, будет иметь встроенную таблицу часто используемых простых чисел и очень быструю реализацию тестирования простоты. Затем вы можете ожидать от компилятора развернуть loop
в основной функции несколько раз, может быть, даже полностью, содержащий только выходные операторы, по существу выполняя преобразование, которое вы ищете.
Других решений пока нет …