C++とJavaとSwiftの速度比較 〜配列編〜

SwiftはAppleが2014年に発表した非常に新しいプログラミング言語です。 コンパイルすることで高速な実行が可能でありながら、スクリプト言語のようなモダンな文法を多く取り入れており、さらにMacだけでなくLinuxでもコンパイル可能。「C++やJavaから乗り換えってありなんだろうか?」と考えた人も少なくないはず。

ただ、乗り換えると言っても「C++やJavaと比べてパフォーマンスはどうなの?」という疑問もあります。ということで、今回はC++やJavaとSwift(ついでにPython)の、配列アクセス観点からの速度比較を行ってみました。

実験環境

今回は AWS EC2 t2.micro インスタンスの Amazon Linux 2023 を使用しました。

OS

$ cat /etc/system-release
Amazon Linux release 2023.4.20240429 (Amazon Linux)

Swift

$ swift --version
Swift version 5.10 (swift-5.10-RELEASE)
Target: x86_64-unknown-linux-gnu

コンパイルオプション

$ swiftc -O

C++

$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-amazon-linux/11/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-amazon-linux
(中略)
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 11.4.1 20230605 (Red Hat 11.4.1-2) (GCC)

コンパイルオプション

$ g++ -O3 -std=c++11

Java

Amazon Linux だったのでJDKもAWS製のCorrettoを使っています。

$ java -version
openjdk version "21.0.3" 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-21.0.3.9.1 (build 21.0.3+9-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.3.9.1 (build 21.0.3+9-LTS, mixed mode, sharing)

デフォルトだとメモリが足りないので実行時に -Xms960m を付けました。

Python

CPythonとPyPyの両方を使ってみました。

CPython

$ python --version
Python 3.12.3

PyPy

$ python --version
Python 3.10.14 (75b3de9d9035, Apr 21 2024, 10:54:48)
[PyPy 7.3.16 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]

ソースコード

今回は配列アクセスの速度比較ということで、配列の要素に順次アクセスし、代入・参照を行うプログラムを用いました。

  • 2の26乗の長さの整数型の配列を確保し、全要素を0で初期化。
  • 配列の各要素にシーケンシャルにアクセスし、数値を代入。
  • 配列の各要素にシーケンシャルにアクセスし、総和を求める。

という処理を10回繰り返すプログラムです。

Swift

SwiftのArrayはアクセスの際に領域外判定を行うため、バグにすぐ気づくことができる一方で処理は多少遅くなると考えられます。

Swiftにも領域の判定を行わない、C++のポインタのような機能もあるようですが、今回の実験には用いていません。

func test_array(N:Int, num:Int) -> Int {
    // 長さNの配列を用意し0で初期化
    var array = Array<Int>(repeating:0,count:N)

    // 各要素にnumを代入
    for i in 0..<N {
        array[i] = num
    }

    // 各要素の和を計算
    var sum = 0
    for i in 0..<N {
        sum += array[i]
    }

    return sum
}

// メイン処理
let N = 1 << 26
let num = 3
for _ in 0..<10 {
    print(test_array(N:N, num:num))
}

C++

C++はnewで確保したintの配列を使ったものと、STLのvectorを使ったものそれぞれを比較に用いました。

配列はvectorと違い便利なメソッドも無く領域外アクセスのリスクも高いです。以下のソースを見てもらえばわかりますが、ソースの可読性も下がり、開発コストは増します。ただその分動作は早いと思われます。

#include <iostream>
#include <vector>
#include <string>

int test_array(const int N, const int num) {
    // 長さNの配列を用意
    int *array = new int[N];
    int sum = 0;

    // 0で初期化
    for (int i = 0; i < N; i++) {
        array[i] = 0;
    }

    // 各要素にnumを代入
    for (int i = 0; i < N; i++) {
        array[i] = num;
    }

    // 各要素の和を計算
    for (int i = 0; i < N; i++) {
        sum += array[i];
    }

    delete [] array;

    return sum;
}

int test_vector(const int N, const int num) {
    // 長さNの配列を用意し0で初期化
    std::vector<int> vector(N, 0);
    int sum = 0;

    // 各要素にnumを代入
    for (auto &a : vector) {
        a = num;
    }

    // 各要素の和を計算
    for (auto a : vector) {
        sum += a;
    }

    return sum;
}

int main(int argc, char *argv[]) {
    const int N = 1 << 26;
    const int num = 3;

    if (argc < 2) return 1;

    std::string arg = argv[1];

    // arrayを指定したら配列の処理を実行
    if (arg == "array") {
        for ( int i = 0; i < 10; i++ ) {
            std::cout << test_array(N, num) << std::endl;
        }
    }

    // vectorを指定したらvectorの処理を実行
    if (arg == "vector") {
        for (int i = 0; i < 10; i++) {
            std::cout << test_vector(N, num) << std::endl;
        }
    }

    return 0;
}

Java

Javaでは配列(int)とArrayList(Integer)のそれぞれを用いています。 ArrayListはSwiftのArray同様に、様々なメソッドを持っており範囲外へのアクセスもチェックしています。

import java.util.ArrayList;
import java.util.Collections;

public class test{
    private static int test_arraylist(int N, int num) {
        // 長さNの配列を用意し0で初期化
        ArrayList<Integer> array = new ArrayList<Integer>(Collections.nCopies(N, 0));

        // 各要素にnumを代入
        for (int i = 0; i < N; i++) {
            array.set(i, num);
        }

        // 各要素の和を計算
        int sum = 0;
        for (int i = 0; i < N; i++) {
            sum += array.get(i);
        }

        return sum;
    }

    private static int test_array(int N, int num) {
        // 長さNの配列を用意
        int[] array = new int[N];

        // 0で初期化
        for (int i = 0; i < N; i++) {
            array[i] = 0;
        }

        // 各要素にnumを代入
        for (int i = 0; i < N; i++) {
            array[i] = num;
        }

        // 各要素の和を計算
        int sum = 0;
        for (int i = 0; i < N; i++) {
            sum += array[i];
        }

        return sum;
    }

    public static void main(String[] args) {
        final int N = 1<<26;
        final int num = 3;

        if (args.length < 1) return;

        // arrayを指定したら配列の処理を実行
        if (args[0].equals("array")) {
            for (int i = 0; i < 10; i++) {
                System.out.println(test_array(N, num));
            }
        }

        // arraylistを指定したらArrayListの処理を実行
        else if (args[0].equals("arraylist")) {
            for (int i = 0; i < 10; i++) {
                System.out.println(test_arraylist(N, num));
            }
        }
    }
}

Python

動作は非常に遅いですが、コードはとてもシンプルに書けます。速度にシビアでなければPythonで十分という場面は多くあります。ただコンパイル無しで比較するのはあまりに不憫なので今回はPyPyによる実行もしてみました。PyPyはJITコンパイラによる最適化が行われるためCPythonに比べてかなり高速になると思われます。

def test_array(N, num):
    # 長さNの配列を用意し0で初期化
    array = N * [0]

    # 各要素にnumを代入
    for i in range(N):
        array[i] = num

    # 各要素の和を計算
    sum = 0
    for a in array:
        sum += a

    return sum

if __name__ == '__main__':
    N = 1 << 26
    num = 3

    print(test_array(N, num))

結果

時間の計測にはtimeコマンドを用い、5回実行したときのそれぞれのユーザーCPU時間の平均を実行時間としました。実行時のブレもあるので細かい差はあまり考えない方が良いと思われます。

言語実行時間(秒)最速との差
Java(配列)2.15-
C++(配列)2.5116.67%
C++(vector)2.5618.71%
Swift5.15139.14%
PyPy6.35194.80%
Java(ArrayList)9.13323.86%
CPython58.352608.87%

それぞれの実行時間をグラフにしました。

言語ごとの実行時間のグラフ

CPythonがあまりに遅すぎてよくわからないので、CPythonを抜いたグラフも載せておきます。

Python以外の言語ごとの実行時間のグラフ

驚きだったのはJavaがC++より速かったということです。もしかしたら実装による最適化の余地や実行時のブレなどがあるかもしれません。ただJavaはJITで実行環境に応じた最適化を行うため、C++の静的な最適化よりも速度が高まるということがある可能性はありそうです。

C++は配列でもvectorでもほぼ速度が変わりませんでした。コードによってコンパイラの最適化の効き具合が変わってくる可能性もありますが、この結果を信じるならばほとんどの場合においてvectorを使った方が良さそうです。

一方Javaは配列とArrayListでは大きく差がつきました。ただし、ここで試しに配列側の実装をintの配列からIntegerの配列に変えたところかなり速度低下したため、これは配列とArrayListというよりintとIntegerの差がかなり効いていそうです。

SwiftとPyPyは領域外アクセスのチェックをしているにも関わらずJavaのArrayListよりも速いという結果となりました。もちろんC++やJavaの配列ほど速くはないのですが、1.4〜2倍程度の差をどう捉えるかは用途によりそうです。個人的にはPythonのランタイムをPyPyにするだけで9倍以上速くなるというのも驚きでした。

おわりに

C++, Java, Swift, Pythonの違いを見てみました。思ったより違う or 思ったより違わない など、いろいろ思うところはあるかと思います。 SwiftがC++やJavaの代わりになるか? という問いに対しても、場合によりけりとしか言いようがないですね。

今回の比較自体が言語の性能のほんの一部を比べただけですし、この結果だけで言語の優劣を論じることはできません。何より言語は用途によって使い分けるべきものですので、その判断のためのよい材料になればと思います。

この書き方の方が速いよ!とか、この比べ方はまずいよ!など、なにか指摘があればコメントいただけると幸いです。

配列アクセスではなくより複雑な処理でも比較した記事もありますのでご興味あれば御覧ください。

余談

プログラミングはその書き方によって効率も大きく変わってきます。以下はC++11/14の機能を使ってより効率的なコードを書くために良い書籍です。基本的な入門書ではなくC/C++をある程度理解していることが必須です。

Effective Modern C++ -- C++11/14プログラムを進化させる42項目
Scott Meyers (著), 千住 治郎 (翻訳)
- amazon.co.jp
最終更新 2024-05-26

広告

本記事はお役に立てたでしょうか。本ブログでは匿名でのコメントや少額から(15円~)の寄付などを受け付けております。もしお役に立てたのであればご支援いただけると大変励みになります。

Built with Hugo
テーマ StackJimmy によって設計されています。