【11種言語比較】プログラミング言語の”処理速度”を考える

processing-speed-of-program

C++,Java,Python,Go,Rust…いろいろあるプログラミング言語の違いのひとつが、処理速度の違い

  • C++は速い
  • Pythonは遅い
  • RustはC++並み

など、いろいろ言われます。

でも、そもそもなんで、プログラミング言語ごとに、処理速度が違ってくるの?やってることそんなに違うのでしょうか?

※2023/12/31 JavaScriptコード修正し時間を再計測しました(速度向上)。ご指摘くださった方ありがとうございました。

目次

プログラミング言語の処理速度になぜ違いがでるのか

全く同じ結果を出すコードを書いたとしても、プログラミング言語によって処理速度は変わってきます

実際の例で紐解きましょう。11個の言語を比較してみます。

11個の言語を比較する
  1. C++
  2. Java
  3. C#
  4. Python
  5. JavaScript
  6. PHP
  7. Ruby
  8. Swift
  9. Kotlin
  10. Go
  11. Rust

どの言語が速くて、どの言語が遅いのか。違いはどこにあるのか。

コンピューターは、ソースコードをそのまま読めるわけじゃない

まず重要なこととして、どのプログラミング言語のコードも、コンピューターは直接理解できません

コンピューターが理解できるのは、マシン語のみです。マシン語とは、こんなコード。

マシン語
7001 0000 7002 0000 1212 800d 1220 800C …

まあ、訳分からないですね。とうてい人には読めない代物です。

なのでプログラムが動くときは、こんなステップを踏みます。

プログラムが動くとき
  • 人が理解できる言語でコードを書き
  • それをマシン語に翻訳」する

この「人が理解できる言語」が、すなわちプログラミング言語です。

翻訳のやりかたによる速度の違い

マシン語への翻訳という作業は、日本人とアメリカ人が会話するシチュエーションでイメージできます。

会話するとき、翻訳の仕方に2種類あるのは、想像つくでしょう。

  • 同時翻訳 日本語で話すと、ほぼ同時に英語に翻訳する
  • 一括翻訳 日本語で書いた内容を、あとでまとめて英語に翻訳する

マシン語への翻訳の仕方で、プログラミング言語は2種類に大別されます。

同時翻訳方式をインタープリタ方式、一括翻訳方式をコンパイラ方式、といいます。

インタープリタ型:すぐ実行できるが遅い

インタープリタ型の言語と特徴は、この通り。

インタープリタ方式
該当する言語:Python, JavaScript, PHP, Ruby
翻訳方法:同時翻訳方式
処理速度:遅い
実行するとき:すぐに実行できる
インタープリタ方式

Pythonなどの言語は、コードを書いたらすぐに実行できます。これはプログラミング言語のエンジンが、コードを1行づつマシン語に翻訳しながら実行してくれるためです。

すぐに実行はできますが、処理速度は遅くなる。毎ステップ翻訳しているからですね。

コンパイラ型:速いが事前コンパイル必要

コンパイラ型の言語と特徴は、この通り。

コンパイラ方式
該当する言語:C++, Java, C#, Swift, Kotlin, Go, Rust
翻訳方式:一括翻訳方式
処理速度:速い
実行するとき:まずコンパイル(=翻訳)が必要
コンパイラ方式

C++などの言語は、コードを書いたら、まずコンパイルする必要があります。このコンパイルの過程で、マシン語に翻訳しているわけですね。

翻訳が終わったら、実行ファイルができます。これはマシン語になっているので、高速に動作するわけです。

正確には、コンパイルしたときに、完全なマシン語になる場合と、中間的な言語になる場合があります。JavaやC#は中間コードになります。

どちらが優れているという話じゃない

では、インタープリタ型とコンパイル型、どちらが優れているの?

というと、そういう話ではないことは分かるでしょう。要は使い道によって使い分ける、ということです。

11言語で処理速度を計測してみる

では実際に、11種類のプログラミング言語で、どれくらい処理速度が違うのか、計測してみましょう。

ライプニッツ公式を計算

題材に使うのは、ライプニッツの公式(Leibniz formula)。円周率πを計算するプログラムです。

ライプニッツの公式とは、以下のような式。

$$ {\displaystyle 1-{\frac {1}{3}}+{\frac {1}{5}}-{\frac {1}{7}}+{\frac {1}{9}}-\cdots ={\frac {\pi }{4}}} $$

1,-1/3,+1/5,…と、繰り返し計算していきます。繰り返し数が多いほど、ほんとうの円周率πの値に近づきます。

計算を1億回繰り返してみます。とほうもない数!でもコンピューターなら余裕です。

開発および実行環境として、LinuxのDockerImageを使います。その他スペックは以下の通り。

OSUbuntu22.04
(Window11上のWSL2)
CPURyzen5
開発環境DockerHubより各Imageを取得
(2023/11時点のlatestバージョン)
開発および実行環境

各言語のソースコードとDocker実行コマンドは次の通りです。

やや専門的な内容なので、興味のある言語を見てください。

C++

C++

古くからある古典的な言語。処理速度を追求するならC++と言われますが…

C++ソースコードと実行コマンド

ベンチマークを取るには、C++11から追加されたchronoを使うと便利です。

#include <iostream>
#include <chrono>
#include <cmath>
#include <iomanip>


// ライプニッツ公式計算
double CalculateLeibnizFormula(int n) {
    double pi_approx = 0.0;
    double sign = 1;
    for (int i = 0; i < n; i++) {
        pi_approx += sign / (2.0 * i + 1.0);
        sign = -sign;
    }
    pi_approx *= 4.0;
    return pi_approx;
}


int main() {
    // 計算する項数(1億回)
    const int n = 100000000;
    // 計測開始時間
    const auto start_time = std::chrono::high_resolution_clock::now();

    // ライプニッツ公式計算
    const double pi_approx = CalculateLeibnizFormula(n);

    // 誤差
    const double calculation_error = fabs(M_PI - pi_approx);
    // 計測終了時間
    const auto end_time = std::chrono::high_resolution_clock::now();
    // 計算時間
    const auto elapsed_millisec
        = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();

    std::cout << "円周率近似値  : " << pi_approx << std::endl;
    std::cout << "計算誤差      : " << calculation_error << std::endl;
    std::cout << "経過時間[msec]: " << elapsed_millisec << std::endl;

    return 0;
}

LinuxでのC++コンパイラといったら、gcc。最適化のため-oオプションを入れます。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz gcc g++ leibniz.cpp -o leibniz_cpp
./leibniz_cpp

Java

Java

Webサーバーサイドの主力であるJava。昔は相当遅かったが、改良を重ねて速くなっています。

Javaソースコードと実行コマンド
import java.time.Duration;
import java.time.Instant;

public class Leibniz {

    // ライプニッツ公式計算
    public static double calculateLeibnizFormula(int n) {
        double piApprox = 0.0;
        double sign = 1.0;
        for (int i = 0; i < n; i++) {
            piApprox += sign / (2.0 * i + 1.0);
            sign = -sign;
        }
        piApprox *= 4.0;
        return piApprox;
    }
    
    public static void main(String[] args) {
        // 計算する項数(1億回)
        final int n = 100000000;
        // 計測開始時間
        Instant startTime = Instant.now();

        // ライプニッツ公式計算
        final double piApprox = Leibniz.calculateLeibnizFormula(n);

        // 誤差
        final double calculationError = Math.abs(Math.PI - piApprox);
        // 計測終了時間
        Instant endTime = Instant.now();
        // 計算時間
        Duration elapsed = Duration.between(startTime, endTime);

        System.out.println("円周率近似値  : " + piApprox);
        System.out.println("計算誤差      : " + calculationError);
        System.out.println("経過時間[msec]: " + elapsed.toMillis());
    }
}

Javaのビルドには、JDK(Java Development Kit)が必要です。ここではOpenJDKを使います。JDKには、AdoptOpenJDK,Eclipse Temurin,Amazon Correttoなど色々ありますが、基本的にはOpenJDKと同じです。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz openjdk javac Leibniz.java

C#

Javaの対抗馬としてMicrosoftが開発した言語です。.NET Coreの登場でLinuxでも動くようになりました。

C#ソースコードと実行コマンド
using System;

class Program
{
    // ライプニッツ公式計算
    static double CalculateLeibnizFormula(int terms)
    {
        double piApprox = 0.0;
        double sign = 1.0;
        for (int i = 0; i < terms; i++)
        {
            piApprox += sign / (2.0 * i + 1.0);
            sign = -sign;
        }
        piApprox *= 4.0;
        return piApprox;
    }

    static void Main()
    {
        // 計算する項数(1億回)
        const int n = 100000000;
        //計測開始
        var sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        // ライプニッツ公式計算
        double piApprox = CalculateLeibnizFormula(n);

        // 誤差
        double calculationError = Math.PI - piApprox;
        // 計測停止
        sw.Stop();

        Console.WriteLine("円周率近似値  : {0}", piApprox);
        Console.WriteLine("計算誤差{0}   : {0}", calculationError);
        Console.WriteLine("経過時間[msec]: {0}", sw.ElapsedMilliseconds);
    }
}

ここでは、.NET FrameworkのLinuxオープン実装である、Monoを使います。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz mono mcs leibniz.cs
./leibniz.exe

Python

AIの登場で一気に普及したPython。オールマイティに使える言語です。

Pythonソースコードと実行コマンド
import math
import time

# ライプニッツ公式計算
def calculate_leibniz_formula(n):
    pi_approx = 0
    sign = 1
    for i in range(n):
        pi_approx += sign / (2 * i + 1)
        sign = -sign
    pi_approx *= 4
    return pi_approx


# 計算する項数(1億回)
n = 100000000
# 計測開始時間
start_time = time.monotonic()

# ライプニッツ公式計算
pi_approx = calculate_leibniz_formula(n)

# 誤差
calculation_error = math.pi - pi_approx
# 計測終了時間
end_time = time.monotonic()
# 計算時間
elapsed_millisec = (end_time - start_time) * 1000

print("円周率近似値  : ", pi_approx)
print("計算誤差      : ", calculation_error)
print("経過時間[msec]: ", elapsed_millisec)

公式DockerImageにpythonがあります。

docker run -it --rm --name python-leibniz -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz python python leibniz.py

JavaScript

Webフロントエンドで必須のJavaScript。Node.jsの登場でバックエンドにも使えるようになっています。

JavaScriptソースコードと実行コマンド
// ライプニッツ公式計算
const calculateLeibnizFormula = (n) => {
    let piApprox = 0;
    let sign = 1;
    for (let i = 0; i < n; i++) {
        piApprox += sign / (2 * i + 1);
        sign = -sign;
    }
    piApprox *= 4;
    return piApprox;
}

// 計算する項数(1億)
const n = 100000000;
// 計測開始時間
startTime = Date.now();

// ライプニッツ公式計算
piApprox = calculateLeibnizFormula(n);

// 誤差
calculationError = Math.abs(Math.PI - piApprox);
// 計測終了時間
endTime = Date.now();
// 計算時間
elapsedMillisec = (endTime - startTime);

console.log(`円周率近似値  : ${piApprox}`);
console.log(`計算誤差      : ${calculationError}`);
console.log(`経過時間[msec]: ${elapsedMillisec}`);

公式DockerImageのnodeを使います。なのでサーバーサイド側での実行となります。ただNode.jsで使っているエンジンはChromeブラウザなどと同じV8なので、ブラウザで実行しても速度は大きくは変わりません。

docker run -it --rm --name js-leibniz -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz node node leibniz.js

PHP

Webを支えてきたスクリプト言語PHP。どれくらいの速さなのか。

PHPソースコードと実行コマンド
<?php
// ライプニッツ公式計算
function calculateLeibnizFormula($n) {
    $pi_approx = 0.0;
    $sign = 1.0;
    for ($i = 0; $i < $n; $i++) {
        $pi_approx += $sign / (2 * $i + 1);
        $sign = -$sign;
    }
    return 4 * $pi_approx;
}

// 計算する項数(1億回)
$n = 100000000;
// 計測開始時間
$startTime = microtime(true);

// ライプニッツ公式計算
$piApprox = calculateLeibnizFormula($n);

// 誤差
$calculationError = pi()-$piApprox;
// 計測終了時間
$endTime = microtime(true);
// 計算時間
$elapsedMillsec = ($endTime - $startTime);

echo "円周率近似値  : {$piApprox}";
echo "計算誤差      : {$calculationError}";
echo "経過時間[msec]: {$elapsedMillsec}";
?>

公式DockerImageであるphpを使います。

docker run -it --rm --name php-leibniz -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz php php leibniz.php

Ruby

Webを支えてきたスクリプト言語Ruby。PHPと差はいかほどか。

Rubyソースコードと実行コマンド
def calculate_leibniz_formula(n)
  pi_approx = 0.0
  sign = 1.0
  for i in 0...n
    pi_approx += sign / (2.0 * i + 1)
    sign = -sign
  end
  4 * pi_approx
end
  
require 'benchmark'

# 計算する項数(1億回)
n = 100000000

# 計測開始
pi_approx = 0
elapsed_sec = Benchmark.realtime do
  # ライプニッツ公式計算
  pi_approx = calculate_leibniz_formula(n)
end

# 誤差
calculation_error = Math::PI - pi_approx
# 計算時間
elapsed_millisec = elapsed_sec * 1000

puts "円周率近似値  : #{pi_approx}"
puts "計算誤差      : #{calculation_error}"
puts "経過時間[msec]: #{elapsed_millisec}"

公式DockerImageであるrubyを使います。

docker run -it --rm --name ruby-leibniz -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz ruby ruby leibniz.rb

Swift

Swiftは、iPhoneやiPadなどのiOSアプリケーション開発で主に使われる言語。

Swiftソースコードと実行コマンド
import Foundation

func calculateLeibnizFormula(n: Int) -> Double {
    var piApprox = 0.0
    var sign = 1.0
    for i in 0..<n {
        piApprox += sign / (Double(i) * 2.0 + 1.0)
        sign = -sign
    }
    return piApprox * 4.0
}

// 計算する項数(1億回)
let n = 100000000
// 計測開始時間
let startTime = Date()

// ライプニッツ公式計算
let piApprox = calculateLeibnizFormula(n: n)

// 誤差
let calculationError = Double.pi - piApprox
// 計算時間
let elapsedMillisec = Date().timeIntervalSince(startTime) * 1000


print("円周率近似値  : \(piApprox)");
print("計算誤差      : \(calculationError)");
print("経過時間[msec]: \(elapsedMillisec)");

公式DockerImageであるswiftを使います。最適化のため-Oオプションを入れます。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz swift swift -O leibniz.swift

Kotlin

Javaを源流とするKotlin。Androidアプリの推奨開発言語です。

Kotlinソースコードと実行コマンド
// ライプニッツ公式計算
fun calculateLeibnizFormula(n: Int): Double {
    var piApprox = 0.0
    var sign = 1.0
    for (i in 0 until n) {
        piApprox += sign / (i * 2 + 1)
        sign = -sign
    }
    return piApprox * 4
}

fun main(args: Array<String>) {
    // 計算する項数(1億回)
    val n = 100000000;
    // 計測開始時間
    val startTime = System.currentTimeMillis()

    // ライプニッツ公式計算
    val piApprox = calculateLeibnizFormula(n)

    // 誤差
    val calculationError = Math.abs(Math.PI - piApprox)
    // 計測終了時間
    val endTime = System.currentTimeMillis()
    // 計算時間
    val elapsedMillisec = endTime - startTime

    println("円周率近似値  : $piApprox");
    println("計算誤差      : $calculationError");
    println("経過時間[msec]: $elapsedMillisec");
}

公式DockerImageであるkotlinを使います。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz zenika/kotlin kotlinc leibniz.kt -include-runtime -d leibniz.jar
docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz zenika/kotlin kotlin leibniz.jar

Go

Googleが開発した言語です。大規模スケールのアプリに対応することを目的としています。

Goソースコードと実行コマンド
package main

import (
    "fmt"
    "math"
    "time"
)

// ライプニッツ公式計算
func calculateLeibnizFormula(n int) float64 {
    piApprox := 0.0
    sign := 1.0
    for i:= 0; i < n; i++ {
        piApprox += sign / (2.0*float64(i) + 1.0)
        sign = -sign;
    }
    piApprox *= 4.0
    return piApprox
}

func main() {
    // 計算する項数(1億)
    n := 100000000
    // 計測開始時間
    startTime := time.Now()

    // ライプニッツ公式計算
    piApprox := calculateLeibnizFormula(n)

    // 誤差
    calculationError := math.Pi - piApprox
    // 計測終了時間
    endTime := time.Now()
    // 計算時間
    elapsed := endTime.Sub(startTime)

    fmt.Printf("円周率近似値  : %v\n", piApprox)
    fmt.Printf("計算誤差      : %v\n", calculationError)
    fmt.Printf("経過時間[msec]: %v\n", elapsed.Milliseconds())
}

公式DockerImageであるgolangを使います。Go言語のコンパイル時はデフォルトで最適化されます。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz golang go run leibniz.go

Rust

C++の性能を持ちかつ安全に使えることを目標とするRust。Linuxカーネル開発にも対応しています。

Rustソースコードと実行コマンド
use std::time::Instant;

// ライプニッツ公式計算
fn calculate_leibniz_formula(n : i32) -> f64 {
    let mut pi_approx: f64 = 0.0;
    let mut sign: f64 = 1.0;
    for i in 0..n {
        pi_approx += sign / (2.0 * i as f64 + 1.0);
        sign = -sign;
    }
    pi_approx * 4.0
}

fn main() {
    // 計算する項数
    let n = 100000000;
    // 計測開始時間
    let start_time = Instant::now();

    // ライプニッツ公式計算
    let pi_approx = calculate_leibniz_formula(n);

    // 誤差
    let calculation_error = std::f64::consts::PI - pi_approx;

    // 計算時間を計測
    let end_time = Instant::now();
    let elapsed = end_time.duration_since(start_time);

    // 結果を表示
    println!("円周率近似値  : {}", pi_approx);
    println!("計算誤差      : {}", calculation_error);
    println!("経過時間[msec]: {}", elapsed.as_millis());
}

公式DockerImageであるrustを使います。なおここではcargoでプロジェクト作成していることを前提としています。–releaseオプションで最適化しています。

docker run --rm -v "$PWD":/usr/src/leibniz -w /usr/src/leibniz rust cargo run --release

処理速度計測結果

全11言語の、円周率計算処理の処理時間を見てみましょう。

言語方式処理時間[ミリ秒]
C++コンパイラ102
Javaコンパイラ104
C#コンパイラ102
Pythonインタープリタ11193
JavaScriptインタープリタ117
PHPインタープリタ2548
Rubyインタープリタ10168
Swiftコンパイラ102
Kotlinコンパイラ106
Goコンパイラ107
Rustコンパイラ103
処理速度計測結果

コンパイラ型言語は速い。インタープリタ型言語は遅い。一目瞭然ですね。

処理速度を見直すときのポイント3つ

プログラミング言語によって、処理速度が異なるのは事実。

でもだからといって、プログラミング言語の選定において、処理速度が決定的な理由になることは、まずないです

処理速度を考えるなら、プログラミング言語の選択よりも大事なことがあります。

  • 処理速度は、I/Oバウンドタスクのほうが影響が大きい
  • ライブラリが性能改善になるときもある
  • プログラミング言語を組み合わせて使うこともできる

I/Oバウンドタスクに注意しよう

コンピューター処理で処理時間がかかるものは、次の2つに区分されます。

コンピューターの処理区分
  • CPUバウンドタスク CPUの能力に影響する処理。計算処理など。
  • I/Oバウンドタスク 入出力の能力に影響する処理。ファイル読み書き、ネットワーク通信など

このうち、プログラミング言語によって差が出るのは、CPUバウンドタスクだけです。

I/Oバウンドタスクは、プログラミング言語の違いには殆ど影響しません。どちらかといえばこのような要素に影響します。

  • OS(Windows,Mac,Linuxなど)
  • ハードウェア(ディスクread/write速度、ネットワーク通信速度など)

そして、処理速度の問題は、I/Oバウンドタスクによって引き起こさるケースが多いのです。

性能問題は、I/Oバウンドタスクの性能向上(入出力データサイズを減らすとか、バックグラウンドで処理するとか)を考えるとか、並行/並列処理を考えたほうがよい場合が多いです。

ライブラリを活用しよう

どのプログラミング言語でも、様々な用途に応じたライブラリが使えます。

速度向上を重視したライブラリも多いので、ライブラリを使うだけで処理速度が向上することも多々あります。

Pythonで考えましょう。有名なライブラリにNumPyがあります。NumPyを使った処理でも計測してみましょう、

import numpy as np

# ライプニッツ公式計算(NumPy利用)
def calculate_leibniz_formula3(n):
    seq = np.array(range(n))
    pi_approx = np.sum( np.where(seq % 2 == 0, 1, -1) / (seq * 2 + 1)) * 4
    return pi_approx
言語処理時間[ミリ秒]
Python11193
Python(NumPy使用)5704
Python計測結果

ライブラリを使っただけで、処理時間が1/2になりました。

特にPythonは、ライブラリを使ってこそ真価を発揮する言語です。積極的に活用しましょう。

プログラミング言語の組み合わせも検討しよう

処理速度が重要であるなら、特に速度が重要なところだけ別のプログラミング言語で組む、という手法もよく採られます。

PythonのライブラリをC++で組む

さきほど出てきたNumPy。Pythonのライブラリですが、実はC言語で実装されています。

Pythonのライブラリを、C++などで組む、というやり方はよく使われます。

JavaScriptの一部処理をWebAssemblyで組む

永らくJavaScriptしか使えなかったWebフロントエンド。近年は、WebAssemblyという技術が登場しています。コンパイラ型に近い処理速度が実現可能で、C、C++、Rustといった言語で開発が可能です。

メインはJavaScript、時間のかかる処理だけRustでWebAssenmby実装、といった組み合わせも可能です。

リアルタイム処理の場合は要注意

処理速度が言語選択の決定的理由ではない、と言いましたが、リアルタイム処理の場合は話が違ってきます

リアルタイム処理とは、遅延が許されない処理、のこと。例えば以下のようなものがあります。

リアルタイム処理の例
  • ロボット制御
  • オンラインゲーム
  • OSカーネル

これらは、常に一定間隔で動作する必要があり、遅れてはならない処理です。遅れとは1秒や2秒といったレベルでなく、ミリ秒(千分の一秒)、マイクロ秒(百万分の一秒)レベルの話になります。

このレベルなら当然コンパイル型言語を使うのですが、更に注意すべきなのが、プログラミング言語が持つガベージコレクト機構です。

ガベージコレクトとは、使わなくなったメモリを裏で自動開放する仕掛け。Java,C#,Swift,Kotlin,Goなどが持っています。メモリ開放を気にしなくていいメリットがあるのですが、いつ動くかは分かりません。これが裏で動いたときに、リアルタイム性に影響を及ぼす場合があるのです。

なのでリアルタイム処理には、ガベージコレクトを持たない C、C++、Rustといった言語が、主に使われます。

C++には「ゼロオーバーヘッド原則」、Rustには「ゼロコスト抽象化」の考えがあります。どちらも、実行するときにガベージコレクトなどの余計なCPUコストをゼロにする、という方針で設計されている言語です。

まとめ

全11種のプログラミング言語で、処理速度の比較を行ってきました。

前述のように、プログラミング言語選択において、処理速度が決定的な理由になることは少ないでしょう。

しかし、プログラミング言語ごとの処理速度の特性を知っておくことは、性能向上に有効です。その背後にある本質を掴んでおきましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次