はじめに
みなさんこんにちは。ハーツテクノロジーの 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>
using namespace std;
#define MAX 101 //小数点以下
int pi[MAX] = { 0 };
int val[MAX] = { 0 };
void val_def(int* q) //calculate val, ‘4/q’を小数点MAXまで計算
{
    int start = 4;
    for (int i = 0; i < MAX; i++) 
    {
        val[i] = start / *q;
        start = (start % *q) * 10;
    }
}
void cal() //calculate pi
{
    int p = -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 (int i = 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]+10
                pi[i] += 10;
                pi[i] += p * val[i];
            }
            else
                pi[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);
    }
}
void print_pi()
{
    printf("%d.", pi[0]);
    for (int i = 1; i < MAX; i++)
    {
        printf("%d", pi[i]);
        if (i % 10 == 0)
            printf("  ");
        if (i % 50 == 0)
            printf("\n  ");
    }
}
int main(void)
{
    double result = 0;
    printf("start\n");
    clock_t begin, end;
    begin = clock();
    cal();
    end = clock();
    print_pi(); //use if you want to see 0 ~ MAX PI value
    result += (double)(end - begin);
    printf("\n");
    printf(" %f sec(not include print time.)", result / CLOCKS_PER_SEC);
}
A-2. C#(計算時間: 458.992秒)
using System;
namespace leibniz
{
    class func
    {
        static int MAX = 101; //小数点以下 100まで
        int[] pi = new int[MAX];
        int[] val = new int[MAX];
        public void val_def(int q)
        {
            int start = 4;
            for (int i = 0; i < MAX; i++)
            {
                val[i] = start / q;
                start = (start % q) * 10;
            }
        }
        public void cal()
        {
            int p = -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 (int i = MAX - 1; i >= 0; i--)
                {
                    if (p == -1 && pi[i] < val[i])
                    {
                        pi[i - 1]--;
                        pi[i] += 10;
                        pi[i] += p * val[i];
                    }
                    else
                        pi[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);
            }
        }
        public void print()
        {
            Console.Write(pi[0] + ".");
            for (int i = 1; i < MAX; i++)
            {
                Console.Write(pi[i]);
                if (i % 10 == 0)
                    Console.Write("  ");
                if (i % 50 == 0)
                    Console.Write("\n  ");
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            double result = 0;
            DateTime begin, end;
            begin = DateTime.Now;
            func _func = new func();
            _func.cal();
            end = DateTime.Now;
            result += end.Subtract(begin).TotalSeconds;
            _func.print(); //use if you want to see 0 ~ MAX PI value
            Console.WriteLine("\n " + end.Subtract(begin).TotalSeconds + " sec");
        }
    }
}
A-3. JavaScript(計算時間: 456.066秒)
let MAX = 101; ////小数点以下 100まで
let pi = Array.apply(null,new Array(MAX)).map(Number.prototype.valueOf,0);
let val = Array.apply(null,new Array(MAX)).map(Number.prototype.valueOf,0);
let begin = new Date().getTime();
let result = 0;
function val_def(q){
    let start = 4;
    for (let i = 0; i < MAX; i++){
        val[i] = Math.floor(start / q);
        start = (start % q) * 10;
    }
}
function cal(){
    let p = -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 (let i = MAX - 1; i >= 0; i--){
            if (p == -1 && pi[i] < val[i]) {
                pi[i - 1]--;
                pi[i] += 10;
                pi[i] += p * val[i];
            }
            else
                pi[i] += p * val[i];
            if (pi[i] >= 10) {
                pi[i - 1] += parseInt(pi[i] / 10);
                pi[i] %= 10;
            }
        }  
        q += 2;
        num++;
        val = [];
    }
}
function print(){
    process.stdout.write(`${pi[0]}.`);
    for (let i = 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();
let end = new Date().getTime();
print(); //use if you want to see 0 ~ MAX PI value
result = (end-begin)/1000;
console.log(`\n${(end-begin)/1000} sec`);
A-4. Python(計算時間: 1148.174秒)
import time
MAX = 11 #遅いため小数点以下 10まで
pi = [0] * MAX
val = [0] * MAX
def cal():
    global pi
    global val
    p = -1
    q = 1
    num = 1
    while num <= 1e8:    #pi = 4 * (1/1 - 1/3 + 1/5 -  ~ 1/(1e8*2-1))
        val_def(q)
        val = list(reversed(val))
        p *= -1
        for index,i in enumerate(pi):
            if p == -1 and pi[index] < val[index]:
                pi[index + 1] -= 1
                pi[index] += 10
                pi[index] += p * val[index]
            else:
                pi[index] += p * val[index]
            if i >= 10:
                pi[index + 1] += pi[index] // 10
                pi[index] %= 10
        q += 2
        num += 1
        val = [0] * MAX
    pi = list(reversed(pi));
def val_def(q):
    global val
    start = 4
    for index,i in enumerate(val):
        val[index] = start // q
        start = (start % q)*10
def print_pi():
    global pi
    print("%d." % pi[0],end="")
    for index,i in enumerate(pi):
        if index == 0:
            continue
        print(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桁
int pi[MAX] = { 0 };
void pi_add( long long  _q, int _a=1 )
{
    long long start = 4;        // _q は 1e9 まで来る想定なので long 32bit では足りない
    for ( int i=0; i<MAX; i++ )
    {
        pi[i] += (start / _q) * _a;
        start = (start % _q) * 10;
    }
}
int main()
{
    printf( "start\n" );
    auto begin = clock();    // 実行時間を計測
    for ( long long q=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 ( int i=MAX-1; i>0; i-- )   // 0以下の値は、上の桁から拝借してくる
    {
        for ( ; pi[i] < 0 ; ) {
            pi[i-1] -= 1;
            pi[i]   += 10;
        }
    }
    for ( int i=MAX-1; i>0; i-- )   // 10以上の値は、上の桁に送る
    {
        if ( pi[i] >= 10 ) {
            pi[i-1] += pi[i] / 10 ;
            pi[i]   %= 10 ;
        }
    }
    // 表示
    printf( "%d.", pi[0]);
    for ( int i = 1; i < MAX; i++)
    {
        printf( "%d", pi[i] );
        if ( i % 10 == 0 ) printf( "  " );
    }
    printf( "\n" );
    printf( " %f sec.", (double)(clock() - begin) / CLOCKS_PER_SEC );    // 実行時間を表示
    return 0 ;
}
B-2. C#(計算時間: 287.459秒)
using System;
namespace leibniz
{
   class Program
   {
       static int MAX = 21;
       static int[] pi = new int[MAX];
       static void pi_add(long _q, int _a = 1)
       {
           long start = 4;
           for (int i = 0; i < MAX; i++)
           {
               pi[i] += (int)(start / _q) * _a;
               start = (start % _q) * 10;
           }
       }
       static void Main(string[] args)
       {
           Console.WriteLine("start");
           DateTime begin = DateTime.Now;
           for (long q = 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 (int i = MAX - 1; i > 0; i--)   // 0以下の値は、上の桁から拝借してくる
           {
               for (; pi[i] < 0;)
               {
                   pi[i - 1] -= 1;
                   pi[i] += 10;
               }
           }
           for (int i = MAX - 1; i > 0; i--)   // 10以上の値は、上の桁に送る
           {
               if (pi[i] >= 10)
               {
                   pi[i - 1] += pi[i] / 10;
                   pi[i] %= 10;
               }
           }
           Console.Write(pi[0] + ".");
           for (int i = 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' )
const begin = Date.now()    // 実行時間を計測
const MAX = 21 // 小数点以下 20桁
var pi = Array( MAX ).fill( 0 )
function pi_add( _q, _a=1 ) // 加算
{
    let start = 4
    for ( let i=0; i<MAX; i++ )
    {
        pi[i] += Math.floor( start / _q ) * _a
        start = ( start % _q ) * 10
    }
}
for ( let q=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 ( let i=MAX-1; i>0; i-- )   // 0以下の値は、上の桁から拝借してくる
{
    for ( ; pi[i] < 0 ; )
    {
        pi[i-1] -= 1
        pi[i]   += 10
    }
}
for ( let i=MAX-1; i>0; i-- )   // 10以上の値は、上の桁に送る
{
    if ( pi[i] >= 10 ) {
        pi[i-1] += parseInt( pi[i] / 10 )
        pi[i]   %= 10
    }
}
// 表示
var r = `${pi[0]}.`
for ( let i=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秒)
import time
print( "start" )
begin = time.time() # 実行時間を計測
MAX = 21 # 小数点以下 20桁
pi  = [0] * MAX
def pi_add( q, a=1 ):
    start = 4
    for i in range( 0, MAX ) :
        pi[i] += start // q * a
        start = (start % q) * 10
q = 1
while 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 i in range( MAX-1, 0, -1 ) :   # 0以下の値は、上の桁から拝借してくる
    while pi[i] < 0 :
        pi[i-1] -= 1
        pi[i]   += 10
for i in range( MAX-1, 0, -1 ) :  # 10以上の値は、上の桁に送る
    if pi[i] >= 10 :
        pi[i-1] += pi[i] // 10
        pi[i]   %= 10
# 表示
print( "%d." % pi[0], end="" )
for i in range( 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()
int main()
{
    printf( "start\n" );
    auto begin = clock();    // 実行時間を計測
    double l = 0.0;
    for ( double n=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 );    // 実行時間を表示
    return 0;
}
C-2. C#(計算時間: 1.847秒)
using System;
namespace Leibniz
{
    class Program
    {
        public static void Main()
        {
            System.Console.WriteLine("start");
            System.DateTime begin = System.DateTime.Now; // 実行時間を計測
            double l = 0.0;
            for (double n = 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' )
const begin = Date.now()    // 実行時間を計測
var l = 0.0
for ( let n=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
}
console.log( l * 4 )
console.log( `${ ( Date.now() - begin ) / 1000 } sec.` )    // 実行時間を表示
C-4. Python(計算時間: 121.914秒)
import time
print( "start" )
begin = time.time() # 実行時間を計測
l = 0.0
n = 1
while 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
print( 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>
int main(void)
{
    int i = 0;
    double val = 1e5; //ループ 回数
    while (i < 5)
    {
        double a = 0;//これのtypeを変える
        int j = 0;
        double result = 0.0;
        while(j < 5)
        {
            a = 0;
            auto begin = clock();
            for (int q = 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)
    }
    return 0;
}
| MinGW(double a) | MinGW(int a) | 
|---|---|
|  |  | 
| VS2019(double a) | VS2019(int a) | 
|---|---|
|  |  | 
- Javascript
let i = 0
let val = 1e5 //ループ 回数
while (i < 5)
{
    let a = 0
    let j = 0
    let result = 0.0
    while (j < 5)
    {
        a = 0;
        let begin = Date.now()
        for (let q = 1; q <= val; q++)
        {
            a++
        }
        result += Date.now() - begin
        j++
    }
    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 を使うのもいいと思いました。
みなさまのご意見もお聞かせください。よろしくお願いいたします。
















