9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

for処理とリスト内包表記とmapとnumpy(とついでにC++)での速度比較

Last updated at Posted at 2021-03-12

結論

 数値計算はnumpyを使うと圧倒的に速い。リスト内包表記を使ったデータ処理は、可読性が悪くなるかもしれないが、コードが短くなり、forループを回すよりも高速になるので、メリットが多い。

本論

 プログラムにおいて計算速度は極めて重要です。あるリストに対して一括して処理を行いたい、という場面は多くあると思いますが、例えばリストの生成についてはforループで逐次処理するよりもリスト内包表記を使う方が速いという報告があります。一括処理の場合はどうでしょうか。

 ここで、0以上1000000未満の整数に対し、500000を引いた後絶対値を取る、という操作を想定し、実際に計算させてみることにします。

limit = int(1e6)
numbers = list(range(1, limit))
np_numbers = np.array(numbers) #numpy用にも

愚直ループ

 一番簡単な方法は、forループを回すことです。

processed_numbers0 = []
for n in numbers:
    processed_numbers0.append(abs(n-limit/2))

 いちいちループを組むのが不格好な感じがありますが、やっている意図は一番伝わりやすいです。

配列サイズを確保

 配列のサイズを逐次変更していくのはなんだか遅そうな感じします。先に配列を確保していく書き方にします。

processed_numbers1 = [0 for n in numbers]
for j,n in enumerate(numbers):
    processed_numbers1[j] = abs(numbers[j]-limit/2)

リスト内包表記

 Pythonに慣れている人だったらリスト内包表記を使いそうです。

processed_numbers2 = [n-limit/2 for n in numbers]

 スッキリしているような、可読性が悪いような、なんとも言えない感じです。

map関数

 リストに対して一括作業をする場合。map関数を使うことも考えられます。イテラブルなオブジェクトに対して一括して関数(正確にはコーラブルなオブジェクト)を作用させる関数です。

processed_numbers3 = list(map(lambda n: abs(n-limit/2), numbers))

 ここでは作用させる関数が組み込み関数1つだけでは表現できないため、ラムダ式を使って独自に定義しています。ラムダ式というと物々しいですが、上のプログラムは下と同値です。

def process(n):
    return abs(n-limit/2)

processed_numbers3 = list(map(process, numbers))

 
 要は、いちいち別に関数を定義するのがめんどくさいので、その場で関数を定義しようぜ、というお気持ちです。書いている方は混乱しないとは思いますが、正直可読性が良いとは言えないでしょう。

numpy

 Pythonで数値計算と言えばnumpyです。

processed_numbers4 = np.abs(np_numbers-limit/2)

 今までのメソッドとの比較で言うと、いちいちforを回したり、リストの存在を意識しなくても計算ができていることがわかります。上のコードでいうnumbersは配列であり、limit/2は変数です。型に厳しい言語ではまず許されないですが、そこら辺はnumpyブロードキャストしていい感じに型変換をしてくれています。numpyが本質的に便利な部分はここなのではないか、とコードを常日頃から書く身としては思います。

計測

 さて、大事なのは計算速度です。早速比較してみましょう。

import random
import numpy as np
import time
import matplotlib.pyplot as plt

limit = int(1e6)
numbers = list(range(1, limit))
np_numbers = np.array(numbers)

labels = ['loop','static', 'compre', 'map', 'numpy']
time_data = [[] for _ in range(5)]

for _ in range(10):
    for i in range(5):
        start = time.time()
        if i == 0:
            processed_numbers0 = []
            for n in numbers:
                processed_numbers0.append(abs(n-limit/2))
        if i == 1:
            processed_numbers1 = [0] * limit
            for j,n in enumerate(numbers):
                processed_numbers1[j] = abs(numbers[j]-limit/2)
        if i == 2:
            processed_numbers2 = [n-limit/2 for n in numbers]
        if i == 3:
            processed_numbers3 = list(map(lambda n: abs(n-limit/2), numbers))
        if i == 4:
            processed_numbers4 = np.abs(np_numbers-limit/2)
        elapsed_time = time.time() - start
        elapsed_time *= 1000
        elapsed_time = int(elapsed_time)
        print("{} {}".format(labels[i].rjust(6, ' '), str(elapsed_time).rjust(4,' ')))
        time_data[i].append(elapsed_time)

print(time_data)

plotrange = range(1, 11)
for i in range(5):
    plt.plot(plotrange, time_data[i], label=labels[i])
plt.legend()
plt.show()


上のコードを走らせると以下のような結果が出ます。

  loop  169
static  184
compre   76
   map  134
 numpy    4
  loop  172
static  194
compre   87
   map  144
 numpy    3
...

image.png

(横軸はイテレーション、縦軸はミリ秒)

 numpyが圧倒的に速いという結果になりました。

 また、ループを回すでは、動的にリストを拡張していく方式と、最初にサイズを確保する方式では、ほとんど処理速度は変わりませんでした。対して、リスト内包表記を使う方法とでは、リスト内包表記の方が倍近く速いという結果になりました。これは既存の実験結果とも一致します。リスト内包表記はコードの記述量を減らす働きもあるので、可読性はさておき、メリットは多いと言えます。

 一番遅いのはmapを使う方法で、なんとリスト内包表記の倍近く時間がかかりました。これは余談ですが、ループを回す方法でも、同じ処理を別関数に定義する方法だと同様に遅くなりました。別関数を呼び出すこと自体がボトルネックになっているようです。 この現象はVSCodeのデバッグモードで実行していたからでした。正確には、愚直ループより少し速いかも程度でした。訂正いたします。(2020/03/13 追記

 **数値の配列ならnumpy一択。**それ以外のデータも扱うような配列であれば、リスト内包表記にすれば速くなりそう、という評価になりました。今回は入力ケースが10^6程度なので僅かな差ですが、これが2重ループ3重ループになれば無視できない、いや場合によっては致命的な差になってきます。

C++で追加実験

 追加実験として、実行速度が速いことに定評のあるC++でも同様の実験をやってみました。こちらでも、動的に配列を変更していく方法(dynamic)と、最初に確保していく方法(static)を試しています。

 の2通りを試しました。

#include <chrono>
#include <iostream>
#include <vector>

using namespace std;
using namespace std::chrono;

int main() {
  vector<int> dynamic_vector_time, static_vector_time;
  int limit = 1e6;
  for (int _ = 0; _ < 10; _++) {
    vector<int> numbers;
    for (int i = 0; i < limit; i++) {
      numbers.push_back(i);
    }
    system_clock::time_point start, end;
    start = system_clock::now();
    vector<int> processed_numbers;
    for (int i = 0; i < limit; i++) {
      processed_numbers.push_back(abs(numbers[i] - limit / 2));
    }
    end = system_clock::now();
    double elapsed = duration_cast<milliseconds>(end - start).count();
    cout << "dynamic method elapsed time is " << elapsed << endl;
    dynamic_vector_time.push_back(elapsed);

    start = system_clock::now();
    vector<int> processed_numbers2(limit);
    for (int i = 0; i < limit; i++) {
      processed_numbers2[i] = numbers[i] - limit / 2;
    }
    end = system_clock::now();
    elapsed = duration_cast<milliseconds>(end - start).count();
    cout << "static method elapsed time is " << elapsed << endl;
    static_vector_time.push_back(elapsed);
  }
  cout << "dynamic vector needed" << endl;
  for (int i : dynamic_vector_time) {
    cout << i << endl;
  }
  cout << "static vector needed" << endl;
  for (int i : static_vector_time) {
    cout << i << endl;
  }
}
dynamic method elapsed time is 17
static method elapsed time is 6
dynamic method elapsed time is 17
static method elapsed time is 6
dynamic method elapsed time is 16
static method elapsed time is 5
...

 numpyと同じぐらい速いです。そして、C++では配列のサイズを最初に確保する方法の方が速いことが確認されました。

9
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?