はじめに
みなさんこんにちは。ハーツテクノロジーの James です。この記事は業務の中で得られた知見から書かれています。
この記事の目的は、開発言語別の実行速度と、おおざっぱな行数(ステップ数)を把握するのが目的です。以下の4種類の言語で、同じアルゴリズムを実装し、その実行速度とコード行数を測ります。今回、調べたかった開発言語は以下の5つです。
- C++ / Visual Studio 2019
- C++ / MinGW(v8.1.0)
- Javascript / node.js(v10.16.3)
- C# / Visual Studio 2019
- Python / Python(v3.7.5)
C++, C#, Javascript, Python の実行速度の計測
計測に使うアルゴリズムには「ライプニッツの公式を使って円周率を求める」を選びました。短いコードですが、それなりにCPUを酷使するので目的は達成できると考えています。
結果的に、コードは3パターン作成し、計測しました。
- A, わたし(James)が書いた、小数点以下100桁で計算するコード
- B, 先輩の山崎さんがリファクタリングした、小数点以下20桁で計算するコード
- C, 同僚のOさんが書いたシンプルにdoubleのみで計算するコードをさらに山崎さんがリファクタリングしたコード
どれも、ライプニッツの公式を使って円周率を計算するコードです。
パターンA わたし(James)が書いた、小数点以下100桁で計算するコード
コードの解説
小数点以下100桁までを、1e8 回繰り返し計算します。
小数点以下100桁まで計算のためpi
(π)とval
(加減する値(1-1/3+1/5-1/7+1/9-⋯))を使います。小数点以下の桁数は設定できます。
- 例)1/3(pi も val と同じ大きさ100桁のint)
int val[] | [0] | [1] | [2] | [3] | [4] | … | [98] | [99] | [100] |
---|---|---|---|---|---|---|---|---|---|
数 | 0 | 3 | 3 | 3 | 3 | … | 3 | 3 | 3 |
1/3と同じ感じの無限小数をvalで保存して、一度は加算(+)一度は減算(-)と繰り返します。piとvalを加減算と同時に一番後から数の上げと下りを計算します。
- int配列使うの理由
double 型は小数点以下15桁の精度まで計算と表記ができますが、今回はもっと多い桁数を計算して表記したかったので int 配列を作成しました。
コード
A-1. C++(計算時間: VS2019-278.973秒, MinGW-165.098秒)
#include <iostream>
#include <time.h>
#include <cstring>
usingnamespacestd;#define MAX 101 //小数点以下
intpi[MAX]={0};intval[MAX]={0};voidval_def(int*q)//calculate val, ‘4/q’を小数点MAXまで計算{intstart=4;for(inti=0;i<MAX;i++){val[i]=start/*q;start=(start%*q)*10;}}voidcal()//calculate pi{intp=-1,q=1,num=1;while(num<=1e8)//pi = 4 * (1/1 - 1/3 + 1/5 - ~ 1/(1e8*2-1)){val_def(&q);p*=-1;for(inti=MAX-1;i>=0;i--){if(p==-1&&pi[i]<val[i])//pi[i]-val[i]時val[i]がpiより高い{pi[i-1]--;//piの高桁からpi[i]+10pi[i]+=10;pi[i]+=p*val[i];}elsepi[i]+=p*val[i];if(pi[i]>=10)//(pi[i] >= 10)時に高桁に数を増加{pi[i-1]+=pi[i]/10;pi[i]%=10;}}q+=2;num++;memset(val,0,MAX);}}voidprint_pi(){printf("%d.",pi[0]);for(inti=1;i<MAX;i++){printf("%d",pi[i]);if(i%10==0)printf(" ");if(i%50==0)printf("\n ");}}intmain(void){doubleresult=0;printf("start\n");clock_tbegin,end;begin=clock();cal();end=clock();print_pi();//use if you want to see 0 ~ MAX PI valueresult+=(double)(end-begin);printf("\n");printf(" %f sec(not include print time.)",result/CLOCKS_PER_SEC);}
A-2. C#(計算時間: 458.992秒)
usingSystem;namespaceleibniz{classfunc{staticintMAX=101;//小数点以下 100までint[]pi=newint[MAX];int[]val=newint[MAX];publicvoidval_def(intq){intstart=4;for(inti=0;i<MAX;i++){val[i]=start/q;start=(start%q)*10;}}publicvoidcal(){intp=-1,q=1,num=1;while(num<=1e8)//pi = 4 * (1/1 - 1/3 + 1/5 - ~ 1/(1e8*2-1)){val_def(q);p*=-1;for(inti=MAX-1;i>=0;i--){if(p==-1&&pi[i]<val[i]){pi[i-1]--;pi[i]+=10;pi[i]+=p*val[i];}elsepi[i]+=p*val[i];if(pi[i]>=10){pi[i-1]+=pi[i]/10;pi[i]%=10;}}q+=2;num++;Array.Clear(val,0,MAX);}}publicvoidprint(){Console.Write(pi[0]+".");for(inti=1;i<MAX;i++){Console.Write(pi[i]);if(i%10==0)Console.Write(" ");if(i%50==0)Console.Write("\n ");}}}classProgram{staticvoidMain(string[]args){doubleresult=0;DateTimebegin,end;begin=DateTime.Now;func_func=newfunc();_func.cal();end=DateTime.Now;result+=end.Subtract(begin).TotalSeconds;_func.print();//use if you want to see 0 ~ MAX PI valueConsole.WriteLine("\n "+end.Subtract(begin).TotalSeconds+" sec");}}}
A-3. JavaScript(計算時間: 456.066秒)
letMAX=101;////小数点以下 100までletpi=Array.apply(null,newArray(MAX)).map(Number.prototype.valueOf,0);letval=Array.apply(null,newArray(MAX)).map(Number.prototype.valueOf,0);letbegin=newDate().getTime();letresult=0;functionval_def(q){letstart=4;for(leti=0;i<MAX;i++){val[i]=Math.floor(start/q);start=(start%q)*10;}}functioncal(){letp=-1,q=1,num=1;while(num<=1e8){//pi = 4 * (1/1 - 1/3 + 1/5 - ~ 1/(1e8*2-1))val_def(q);p*=-1;for(leti=MAX-1;i>=0;i--){if(p==-1&&pi[i]<val[i]){pi[i-1]--;pi[i]+=10;pi[i]+=p*val[i];}elsepi[i]+=p*val[i];if(pi[i]>=10){pi[i-1]+=parseInt(pi[i]/10);pi[i]%=10;}}q+=2;num++;val=[];}}functionprint(){process.stdout.write(`${pi[0]}.`);for(leti=1;i<MAX;i++){process.stdout.write(`${pi[i]}`);if(i%10==0)process.stdout.write("");if(i%50==0)process.stdout.write("\n");}}cal();letend=newDate().getTime();print();//use if you want to see 0 ~ MAX PI valueresult=(end-begin)/1000;console.log(`\n${(end-begin)/1000} sec`);
A-4. Python(計算時間: 1148.174秒)
importtimeMAX=11#遅いため小数点以下 10まで
pi=[0]*MAXval=[0]*MAXdefcal():globalpiglobalvalp=-1q=1num=1whilenum<=1e8:#pi = 4 * (1/1 - 1/3 + 1/5 - ~ 1/(1e8*2-1))
val_def(q)val=list(reversed(val))p*=-1forindex,iinenumerate(pi):ifp==-1andpi[index]<val[index]:pi[index+1]-=1pi[index]+=10pi[index]+=p*val[index]else:pi[index]+=p*val[index]ifi>=10:pi[index+1]+=pi[index]//10pi[index]%=10q+=2num+=1val=[0]*MAXpi=list(reversed(pi));defval_def(q):globalvalstart=4forindex,iinenumerate(val):val[index]=start//qstart=(start%q)*10defprint_pi():globalpiprint("%d."%pi[0],end="")forindex,iinenumerate(pi):ifindex==0:continueprint(i,end="");if(index%10)==0:print(" ",end="")if(index%50)==0:print(" ")print("")if__name__=='__main__':print("start")begin=time.time()cal()end=time.time()print_pi()print(end-begin," sec")
パターンB, 先輩の「山崎さん」がリファクタリングした、小数点以下20桁で計算するコード
コードの解説
小数点以下20桁までを、分母が 1e9 になるまで繰り返し計算します。
コード
B-1. C++(計算時間: VS2019-216.595秒, MinGW-134.391秒)
#include <stdio.h> // printf()
#include <time.h> // clock()
#define MAX 21 //小数点以下 20桁
intpi[MAX]={0};voidpi_add(longlong_q,int_a=1){longlongstart=4;// _q は 1e9 まで来る想定なので long 32bit では足りないfor(inti=0;i<MAX;i++){pi[i]+=(start/_q)*_a;start=(start%_q)*10;}}intmain(){printf("start\n");autobegin=clock();// 実行時間を計測for(longlongq=1;q<=1e9;)// pi = 4/1 - 4/3 + 4/5 - 4/7 + ... 4/1e9{pi_add(q);// 加算q+=2;pi_add(q,-1);// 減算q+=2;}// 計算のあと、各桁を 0 - 9 の範囲に調整するfor(inti=MAX-1;i>0;i--)// 0以下の値は、上の桁から拝借してくる{for(;pi[i]<0;){pi[i-1]-=1;pi[i]+=10;}}for(inti=MAX-1;i>0;i--)// 10以上の値は、上の桁に送る{if(pi[i]>=10){pi[i-1]+=pi[i]/10;pi[i]%=10;}}// 表示printf("%d.",pi[0]);for(inti=1;i<MAX;i++){printf("%d",pi[i]);if(i%10==0)printf(" ");}printf("\n");printf(" %f sec.",(double)(clock()-begin)/CLOCKS_PER_SEC);// 実行時間を表示return0;}
B-2. C#(計算時間: 287.459秒)
usingSystem;namespaceleibniz{classProgram{staticintMAX=21;staticint[]pi=newint[MAX];staticvoidpi_add(long_q,int_a=1){longstart=4;for(inti=0;i<MAX;i++){pi[i]+=(int)(start/_q)*_a;start=(start%_q)*10;}}staticvoidMain(string[]args){Console.WriteLine("start");DateTimebegin=DateTime.Now;for(longq=1;q<=1e9;)// pi = 4/1 - 4/3 + 4/5 - 4/7 + ... 4/1e9{pi_add(q);q+=2;pi_add(q,-1);q+=2;}for(inti=MAX-1;i>0;i--)// 0以下の値は、上の桁から拝借してくる{for(;pi[i]<0;){pi[i-1]-=1;pi[i]+=10;}}for(inti=MAX-1;i>0;i--)// 10以上の値は、上の桁に送る{if(pi[i]>=10){pi[i-1]+=pi[i]/10;pi[i]%=10;}}Console.Write(pi[0]+".");for(inti=1;i<MAX;i++){Console.Write(pi[i]);if(i%10==0)Console.Write(" ");if(i%50==0)Console.Write("\n ");}Console.WriteLine("\n{0} sec.",DateTime.Now.Subtract(begin).TotalMilliseconds/1000);//時間表示}}}
B-3. JavaScript(計算時間: 123.56秒)
console.log('start')constbegin=Date.now()// 実行時間を計測constMAX=21// 小数点以下 20桁varpi=Array(MAX).fill(0)functionpi_add(_q,_a=1)// 加算{letstart=4for(leti=0;i<MAX;i++){pi[i]+=Math.floor(start/_q)*_astart=(start%_q)*10}}for(letq=1;q<=1e9;)// pi = 4/1 - 4/3 + 4/5 - 4/7 + ... 4/1e9{pi_add(q)// 加算q+=2pi_add(q,-1)// 減算q+=2}// 計算のあと、各桁を 0 - 9 の範囲に調整するfor(leti=MAX-1;i>0;i--)// 0以下の値は、上の桁から拝借してくる{for(;pi[i]<0;){pi[i-1]-=1pi[i]+=10}}for(leti=MAX-1;i>0;i--)// 10以上の値は、上の桁に送る{if(pi[i]>=10){pi[i-1]+=parseInt(pi[i]/10)pi[i]%=10}}// 表示varr=`${pi[0]}.`for(leti=1;i<MAX;i++){r+=`${pi[i]}`+((i%10==0)?'':'')}console.log(r)console.log(`${(Date.now()-begin)/1000} sec.`)// 実行時間を表示
B-4. Python(計算時間: 3722.021秒)
importtimeprint("start")begin=time.time()# 実行時間を計測
MAX=21# 小数点以下 20桁
pi=[0]*MAXdefpi_add(q,a=1):start=4foriinrange(0,MAX):pi[i]+=start//q*astart=(start%q)*10q=1whileq<=1e9:# pi = 4/1 - 4/3 + 4/5 - 4/7 + ... 4/1e9
pi_add(q)# 加算
q+=2pi_add(q,-1)# 減算
q+=2# 計算のあと、各桁を 0 - 9 の範囲に調整する
foriinrange(MAX-1,0,-1):# 0以下の値は、上の桁から拝借してくる
whilepi[i]<0:pi[i-1]-=1pi[i]+=10foriinrange(MAX-1,0,-1):# 10以上の値は、上の桁に送る
ifpi[i]>=10:pi[i-1]+=pi[i]//10pi[i]%=10# 表示
print("%d."%pi[0],end="")foriinrange(1,MAX):print(pi[i],end="")if(i%10)==0:print(" ",end="")print("")print(time.time()-begin," sec.")# 実行時間を表示
パターンC, 同僚の「Oさん」が書いたシンプルにdoubleのみで計算するコードをさらに山崎さんがリファクタリングしたコード
3回(小数点以下double(15)まで、1e9まで計算)
コード
C-1. C++(計算時間: VS2019-2.6秒, MinGW-0.608秒)
#include <stdio.h> // printf()
#include <time.h> // clock()
intmain(){printf("start\n");autobegin=clock();// 実行時間を計測doublel=0.0;for(doublen=1;n<1e9;)// l = 1/1 - 1/3 + 1/5 - 1/7 + ... 1/1e9{l+=1.0/n;n+=2;l-=1.0/n;n+=2;}printf("%1.16f\n",l*4);printf(" %f sec.",(double)(clock()-begin)/CLOCKS_PER_SEC);// 実行時間を表示return0;}
C-2. C#(計算時間: 1.847秒)
usingSystem;namespaceLeibniz{classProgram{publicstaticvoidMain(){System.Console.WriteLine("start");System.DateTimebegin=System.DateTime.Now;// 実行時間を計測doublel=0.0;for(doublen=1;n<1e9;)// l = 1/1 - 1/3 + 1/5 - 1/7 + ... 1/1e9{l+=1.0/n;n+=2;l-=1.0/n;n+=2;}System.Console.WriteLine(l*4);System.Console.WriteLine("{0} sec",System.DateTime.Now.Subtract(begin).TotalMilliseconds/1000);}}}
C-3. JavaScript(計算時間: 0.756秒)
console.log('start')constbegin=Date.now()// 実行時間を計測varl=0.0for(letn=1;n<1e9;)// l = 1/1 - 1/3 + 1/5 - 1/7 + ... 1/1e9{l+=1.0/nn+=2l-=1.0/nn+=2}console.log(l*4)console.log(`${(Date.now()-begin)/1000} sec.`)// 実行時間を表示
C-4. Python(計算時間: 121.914秒)
importtimeprint("start")begin=time.time()# 実行時間を計測
l=0.0n=1whilen<1e9:# l = 1/1 - 1/3 + 1/5 - 1/7 + ... 1/1e9
l+=1.0/nn+=2l-=1.0/nn+=2print(l*4)print(time.time()-begin," sec.")# 実行時間を表示
結果
実行環境
- CPU : Intel Core i7-7500U 2.70GHZ
- RAM : 8 GB
- SSD : 250 GB
- OS : Windows 10 Pro
実行速度(秒)
code | VS2019/C++ | MinGW/C++ | node.js/Javascript | VS2019/C# | Python |
---|---|---|---|---|---|
A | 278.973 | 165.098 | 456.066 | 458.992 | 1148.174 |
B | 216.595 | 134.391 | 123.56 | 287.459 | 3722.021 |
C | 2.6 | 0.608 | 0.756 | 1.847 | 121.914 |
やはり C++ がよい成績を出しました。でも、2番目に Javascript が速いです。これは予想外。。。Python は3位のC#より3倍以上、最大で約100倍遅いです。
追加で、MinGW と Visual Studio の速度の違いも意味があると思います。どちらも「O2」最適化オプションで計測しました。
コード量(行数)
コード量はコメント業も空白行も含む純粋な行数です。好みの書き方に左右されるので、あくまでも参考値です。
code | C++ | Javascript | C# | Python |
---|---|---|---|---|
A | 75 | 56 | 78 | 59 |
B | 59 | 51 | 63 | 41 |
C | 24 | 15 | 25 | 16 |
コード量は C++, C# が長めで Python, Javascript 短めに思います。
3つとも Javascript と Python は長さ面で似ていましたが、速度面では反対の感じに結果がでました。
総評
この計測の前に g++(C++ compiler) で「-O2」オプションを使わずに計測しました。その計測のときにはJavascriptが速度面で1位でした。
でも最適化オプション「-O2」を使ったあとで結果が変わりました。
しかし速度だけでなくコード量を一緒に見れば Javascript が合理的な言語だと思いました。
追加テスト
ABC、3つの中で1つ(B,山崎さんのコード)だけ Javascriptが速い結果が出ました。それもなぜJavascriptが速いかが解決してないので簡単なテストをしました。
ループの速度が違うのでループを1回は1e5反復,2回は1e6反復して5回1e9までテストしました。(オプションで -O2使う)
ループ速度
コード
- C++
#include <iostream>
#include <time.h>
intmain(void){inti=0;doubleval=1e5;//ループ 回数while(i<5){doublea=0;//これのtypeを変えるintj=0;doubleresult=0.0;while(j<5){a=0;autobegin=clock();for(intq=1;q<=val;q++)//これのtypeを変える{a++;}result+=clock()-begin;j++;}printf("long long q ~ 10^%d, double a = %10.0f: %.3f sec \n",i+5,a,(result/5)/CLOCKS_PER_SEC);i++;val*=10;//ループ 回数増加(i=0:1e5, i=1:1e6, i=2:1e7, i=3:1e8, i=4:1e9)}return0;}
MinGW(double a) | MinGW(int a) |
---|---|
![]() | ![]() |
VS2019(double a) | VS2019(int a) |
---|---|
![]() | ![]() |
- Javascript
leti=0letval=1e5//ループ 回数while(i<5){leta=0letj=0letresult=0.0while(j<5){a=0;letbegin=Date.now()for(letq=1;q<=val;q++){a++}result+=Date.now()-beginj++}console.log(`10^${i+5}: ${((result/5)/1000).toFixed(3)} sec, a = ${a}`)i++val*=10//ループ 回数増加(i=0:1e5, i=1:1e6, i=2:1e7, i=3:1e8, i=4:1e9)}
結果
10^9反復する場合だけ結果を整理します。(単位: 秒)
int q | double q | long long q | |
---|---|---|---|
MinGW - int a | 0.791 | 1.182 | 0.622 |
MinGW - double a | 1.166 | 1.192 | 1.188 |
VS2019 - int a | 4.188 | 1.374 | 4.332 |
VS2019 - double a | 4.855 | 1.365 | 5.582 |
Javascript - type関係ない | 1.019 | - | - |
総合的にVS2019はループカウント(q)のデータタイプがdouble時には一番速かったけどそれも10^7からはJavascriptより遅くなりました。
でもMinGWはdoubleを使う時だけ(aかqかは関係ない)10^7からJavascriptより遅くなりましたが、doubleを使用しない時(aとq全体)にはJavascriptより全体の場合で速くなりました。
VS2019がループカウント(q)をdoubleで使う時だけMinGWと同じ実行時間が出ましたが、intとlong longを使う時にはVS2019がMinGWより3~4倍ぐらい遅くなりました。
まとめに
まだ結果について明快な解答がありません。でも個人的にJavascriptが結構いい言語ということを知りました。
このあと Python がどこまで発展するかまだわからないが、今は速度が必要で、資金など資源が不足ぎみなら C++ より Javascript を使うのもいいと思いました。
みなさまのご意見もお聞かせください。よろしくお願いいたします。