読者です 読者をやめる 読者になる 読者になる

亀岡的プログラマ日記

京都のベッドタウン、亀岡よりだらだらとお送りいたします。

高速を謳う列挙演算ライブラリ Nessos.Streams をC# で試してみた

本記事は、C# Advent Calendar 2014 の12/4日分の記事です。

昨日はgorn708@githubさんによる、C#によるCox回帰の適用の実装 | Grayrecordでした。こんなふうに式定義をサラサラと実装に移していけるプログラミング能力は、鍛えていかないトナーと思いますね。。。

Nessos社について

さて皆さんはNessos社、あるいはMBraceをご存知でしょうか?

Nessos社はギリシャアテネ大学の研究チームが中心となっているソフトウェア研究会社で、MBraceは彼らの主要な仕事の一つです。 MBraceは、Azure上でクラウドコンピューティングをF#で行うためのフレームワークで、コンピュテーション式によって、まるで普通にコードを書いているだけでMapReduceのような並列演算を定義できるF#らしい頭おかしい系のライブラリです。詳しくは以下のリンクやスライドを見てみて下さい。

このMBraceをサポートするライブラリとして、MessagePackやProtocolBuffersよりも軽量を謳うシリアライザFsPicklerや、今回紹介するStreamsなど各種ライブラリを公開しています。名前からもうすうす察しがつくように、F#がメインターゲットとなっているのですが、、、これはF# Advent Calendarではないので、StreamsをC#で動かして遊んでみたいと思います。

Streamsとは

Streamsとは、列挙演算ライブラリ、即ちLinq系の演算の代替ができるライブラリです。

そして名前からピンとくる方がいらっしゃるかもしれませんが、これはJava 8のStreamライブラリから影響を受けているようです。

The main design behind Streams is inspired by Java 8 Streams (上記HPより)

実はNessos社とアテネ大学とで、Javaと.NETのラムダ実装に関する論文が出されており、その中でJavaのラムダのほうがオーバーヘッドが少なく高速である旨が述べられています。

このため、JavaのStream実装を参考に.NETの列挙操作を再実装した、みたいな雰囲気のようです。

C#での使い方

ホームページを見ても分かるように、F#での使用をまずは想定していそうです、、、。が、大丈夫です、ちゃんとCSharp用にサポートライブラリを切ってくれています。 てなわけで、Nugetで簡単に導入できます。

PM> Install-Package Streams.CSharp

では、これでどんな感じで使えるのか、サンプルコードを見てみましょう。こんな感じです。

    var input = Enumerable.Range(1, 10000000).Select(i => (long)i).ToArray();
    var sum = input.AsStream()
        .Where(i => i % 2 == 0)
        .Select(i => i + 1)
        .Sum();

てなわけで、AsStream拡張メソッドでStreams型に変換してからは、ほぼほぼ通常のLinqメソッドが使えます。 もちろん、クエリ式も使えます。

    var input = Enumerable.Range(1, 10000000).Select(i => (long)i).ToArray();
    var query = from x in input.AsStream()
                where x % 2 == 0
                select x;
    query.Sum();

これは簡単ですね!

んで、ほんとに高速なの?

簡単に使えることは分かりました、、、が、本当に高速なのでしょうか?? HPによれば、F#を使えば

Streamsライブラリ Real: 00:00:00.044 CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0
Seq Real: 00:00:00.264 CPU: 00:00:00.265, GC gen0: 0, gen1: 0, gen2: 0
Array Real: 00:00:00.217 CPU: 00:00:00.202, GC gen0: 0, gen1: 0, gen2: 0

と非常に高速そうなのですが。

まぁ、測ってみましょう。

まずは、超単純な可算から。

    input = Enumerable.Range(1, 10000000).Select(i => (long)i).ToArray();
    var dummy = input.Sum(); // System.Linq
    var dummy2 = input.AsStream().Sum();   //Nessos.Streams
Linq 85ms
Streams 37ms

おぉ!速い!これは期待が持てますねー、ではでは、次はWhereとSelectも打っちゃいましょう。

    input = Enumerable.Range(1, 10000000).Select(i => (long)i).ToArray();
    // System.Linq
    var dummy = input
    .Where(i => i % 2 == 0)
    .Select(i => i   + 1)
    .Sum();
    
    //Nessos.Streams
    var dummy2 = input.AsStream()
    .Where(i => i % 2 == 0)
    .Select(i => i + 1)
    .Sum();
Linq 120ms
Streams 268ms

あ。。。れ。。。??

圧倒的に遅いですねー。WhereやSelect片方だけ消したりとかもしてみましたがダメでした。まじで。C#がすごいのか、NessosがC#サボってるのか、、、

んでですね、Sumが速いんで試しにAggregateで同等コード書いてみました。

    input = Enumerable.Range(1, 10000000).Select(i => (long)i).ToArray();
    // System.Linq
    var dummy = input
            .Aggregate(0L, (acc, element) => element % 2 == 0 ? acc + element : acc);

    // Nassos.Streams
    var dummy2 = input.AsStream()
            .Aggregate(0L, (acc, element) => element % 2 == 0 ? acc + element : acc);
Linq 177ms
Streams 152ms

お前集約計算だけやたら早いな!!

ふーむ、C#で使うにはまだまだなのか、それとももっと真面目な計算を回してやらないと効率がでてこないのか、うーん。 少なくともSumやAverage、Max/Minなどの集約系演算を行う分には、使いでがあるのかなーと思います。

というわけで、ぼんやりとした結論になってしまいましたが、次はharipoさんです。OminiSharpネタは期待!