LoginSignup
2

More than 5 years have passed since last update.

ふわっとStreamで試したいこと試してみた。

Last updated at Posted at 2017-12-11

なんとなくアドヴェントカレンダー参加したはいいものの、書くネタがないので書けそうなStreamの話でもしようと思う。今回試したコードはGitHubにもおいておく。

可読性の面

Java8で追加されたコイツ。もう知っている人が多いかもしれないが、ベンチマークをして実行時間を測ってみると扱う要素が小さい場合、手続き型と比べる実行時間が遅くなる。その分、メソッド名が明確で、そのソースが何をしたいのかがわかりやすいというメリットがある。

簡易ベンチマーク実行環境

ツールを使ったり、回数を100万とかやるガチガチのベンチマークする気はない
nanoTimeを使えと言われそうだけど、差がわかるくらいでいいのでcurrentTimeMillisにした。

Bench.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Excution time:"+benchMark()+"[sec]");
    }

    private static double benchMark(){
        long start = System.currentTimeMillis();
        HogeSomeTask task = new HogeSomeTask();
        task.something_do();
        long end = System.currentTimeMillis();

        return ((end - start) / 1000.0);
    }
}

OS:Ubuntu 16.04
CPU:Intel Core i7-2700K CPU @ 5.9GHz(古いのは許して欲しい)
JDK:Open-jdk9

サンプルコード

以下のコードは条件として、5文字未満の対象データを出力する処理のコードとベンチマークをした結果である。これと逆に5文字以上のデータを出力する処理も試したけど、結果はほぼ同じだったので省略。

Procedural.java
    public void use_for(){ 
        List<String>list = Arrays.asList("Java","Ruby","Csharp","Scala","Haskell");
        for(String lang : list){
            if(lang.length() < 5){
                System.out.println(lang);
            }
        }
    } 

10回平均実行時間は0.001[sec]。かなり速い

Streamを使う

Stream.java
 public void use_stream(){ 
        List<String>list = Arrays.asList("Java","Ruby","Csharp","Scala","Haskell");
        list.stream().filter(lang -> lang.length() < 5).forEach(System.out::println);
 }

10回平均実行時間は0.025[sec]。手続き型と比べて少し遅くなっている。

Java9で、Streamにも新機能が出た

今更な感じもあるけど、メソッドについて軽くおさらいしておこう。take/dropWhileメソッドによってScalaっぽく書ける。

takeWhile

対象とする条件を指定するだけで対象データ(条件一致している間)の処理が可能なメソッド。中間処理が減った。

takeWhileExample.java
   List<String>list = Arrays.asList("Java","Ruby","Csharp","Scala","Haskell")   
         list.stream().takeWhile(lang -> lang.length() < 5).forEach(System.out::println);

dropWhile

対象とする条件を指定するだけで対象データ(条件一致以降)の出力が可能なメソッド。takeWhile同様、中間処理が減った。

dropWhileExample.java
  List<String> list = Arrays.asList("Java","Ruby","Csharp","Scala","Haskell");
        list.stream().dropWhile(lang -> lang.length() < 5).forEach(System.out::println);

ofNullable

対象のデータがnullではない場合は、Streamを返送。nullの場合は、空のStreamを返送するメソッド。
以下のようにOptionalから直接Streamを書けるようになった

optional.java
Optional.ofNullable(null).stream().forEach(System.out::println);

気になる速さは

すべて試してベンチマークしてみたので、表でまとめた。ofNullableに関しては、nullを安全に扱う点が大きいらしいので、データを扱うパフォーマンスを検証してる今回においては、アドカレの時間もないので省略させてもらう。

1.上のサンプルコードの処理

平均実行時間(10回)
手続き型 0.001[sec]
Stream 0.025[sec]
parallelStream 0.026[sec]
takeWhile 0.026[sec]
takeWhile(parallelStream使用) 0.032[sec]

Java8からあるメソッドと比較しても大差は無い。中間処理が遅くしているようだ

2. 1と逆の処理

平均実行時間(10回)
手続き型 0.001[sec]
Stream 0.023[sec]
parallelStream 0.031[sec]
dropWhile 0.024[sec]
dropWhile(parellelStream使用) 0.028[sec]

上に同じ

手続き型に負ける理由

おおまかに言うと、手続き型は処理がほぼそのまま素直に書き出されてjdkでコンパイルされるので、中間処理をデータが単純でも行うStreamは遅くなるようだ。

補足

遅い根拠をはっきりさせたい。そのためにStreamメソッドに書いてある処理をIntelliJの機能でどんどん辿ってみた。細かいメソッド呼び出しに分割されたり、遅延実行などの仕組みが遅くしている。

Streamが速い時はあるのか

どういう時に書けばいいか、そんなのはもう読みやすいStreamだけ選んでればいいし、遅いと言っても差は大したものではない。ただ、パフォーマンス的に言う話なら、扱う要素が大きい場合に効力を発揮しそうなので試してみた。

条件

テスト用データとして、アルファベット大文字と数字の20文字からなるランダムな文字列を100万要素作成する。

BigData.java
Random r = new Random(2111);
List<String> data = range(0, 1_000_000)
    .mapToObj(i->
        r.ints().limit(20)
            .map(n -> Math.abs(n) % 36)
            .map(code -> (code < 10) ? '0' + code : 'A' + code - 10)
            .mapToObj(ch -> String.valueOf((char)ch))
            .toString())
    .collect(toList());

この要素から、数字のみを抜き出し、その合計が30以下を抜き出す処理を行う。

手続き型のサンプル

Procedural.java
  public static long use_for(List<String> data){
        long result = 0;
        for(String d : data){
            String numOnly = d.replaceAll("[^0-9]", "");
            if(numOnly.isEmpty()) continue;
            int total = 0;
            for(char ch : numOnly.toCharArray()){
                total += ch - '0';
            }
            if(total >= 30) continue;
            long value = Long.parseLong(numOnly);
            result += value;
        }
        return result;
    }

Streamのサンプル

Stream.java
 public static long streamSum(List<String>data){
        return data.stream()
                .map(d -> d.replaceAll("[^0-9]", ""))
                .filter(d -> !d.isEmpty())
                .filter(d -> d.chars().map(ch -> ch - '0').sum() < 30)
                .mapToLong(d -> Long.parseLong(d)).sum();
 }

takeWhile/dropWhileも試す

ただのstream,parallelStreamと比べてどうなるのか気になるので試す。

takeWhileSample.java
  public static long takeWhileSum(List<String> data){
        return data.stream()
                .map(d -> d.replaceAll("[^0-9]", ""))//数字以外を取り除く
                .takeWhile(d -> !d.isEmpty())
                .takeWhile(d -> d.chars().map(ch -> ch - '0').sum() < 30)//数字の合計が30より小さい
                .mapToLong(d -> Long.parseLong(d)).sum();
  }
dropWhileSample
    public static long dropWhileSum(List<String> data){
        return data.stream()
                .map(d -> d.replaceAll("[^0-9]", ""))
                .dropWhile(d -> d.isEmpty())
                .dropWhile(d -> d.chars().map(ch -> ch - '0').sum() > 30)
                .mapToLong(d -> Long.parseLong(d)).sum();
   }

ベンチマークの結果

平均実行時間(10回)
手続き型 2.132[sec]
parallelStream 1.321[sec]
Stream 2.107[sec]
takeWhile 0.457[sec]
takeWhile(parallelStream) 1.325[sec]
dropWhile 2.175[sec]
dropWhile(parallelStream) 1.377[sec]

ついに真価が見えた気がする。takeWhileがずば抜けて速い。
dropWhileは手続き型と変わらないくらいだったが、parallelStreamから呼び出すことでだいぶ良くなった。

結論

  • Streamで書くのが当たり前だがベスト。
  • parallelStreamはstreamより中間処理が効率的(?)
  • パフォーマンスは要素が大きい場合によい結果が出てくる。
  • takeWhileメソッドは更に中間処理の効率がよくなっている。
  • dropWhileメソッドはparallelStreamから呼び出す方がパフォーマンス的にはベター

以上。気になっていた事を試せた良い機会だった。

参考にした記事

Java8のStreamの目的と書きやすさや可読性、並行処理の効果について
C++, Java, Pythonでプログラムの実行結果を計測してみる.

など。

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
2