🚀

C# vs Python 実行速度論争

2022/10/18に公開

2018年ごろ、「データの処理であれば、C#のLinq よりも Pythonの方が2倍速くなる」という記事が公開され、青々な若造な私が絡んじゃったことがありました。
記録として2018/4/19の記事と2018/4/25の記事をまとめました。
生暖かい目で読んでいただけますと幸いです。


C# の Linq が python の2倍遅い、は嘘

2018/4/19

C#でLinqを使うよりPythonの方が2倍速かったのでベンチマークをしてみた

うん、まあ Linq はそこそこリッチなので、こういう使い方したらそこそこ遅くなるよね。特に GroupBy は遅い。

でもそんなことより ToList() しまくってて遅延実行の意味なし。最初の結果テーブルで C# が3倍ほど遅いのはほぼこれのせいじゃないかな。

あとシリアライザが Newtonsoft。そりゃ遅いよね、って感じ。最近のスタンダード、Utf8Json を使います。

とりあえず自分の環境で python3.6 動かした結果を貼る:

  1. 0.8537077903747559[sec]
  2. 0.8066554069519043[sec]
  3. 0.8596947193145752[sec]

0.83 秒くらいですかね?

んで C# 、愚直に書いてみる。

using System;
using System.Linq;
using System.Runtime.Serialization;
using System.IO;
using System.Diagnostics;
using Utf8Json;

namespace csharp
{
    class Program
    {
        private static readonly string _csvPath = @"C:\Users\****\Documents\work\private\real-test\test.csv";

        static void Main(string[] args)
        {
            var outPath = Path.Combine(Path.GetDirectoryName(_csvPath), "result_csharp.json");
            if (File.Exists(outPath))
            {
                File.Delete(outPath);
            }

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            var lines = File.ReadLines(_csvPath)
                .Skip(1)
                .Select(line => {
                    var parts = line.Split(",");
                    float x = float.Parse(parts[2]);
                    float y = float.Parse(parts[3]);
                    int z = x > 0
                            ? (int)(x * y + 0.0000001)
                            : (int)(x * y - 0.0000001);
                    return (a: parts[0], z: z);
                })
                .GroupBy(i => i.a, i => i.z)
                .Select(g => (a: g.Key, z: g.Sum()));

            var json = JsonSerializer.SerializeUnsafe(lines);
            using (var fileStream = File.OpenWrite(outPath))
            {
                fileStream.Write(json.Array, 0, json.Count);
            }
            Console.WriteLine($"{stopwatch.Elapsed}sec");
        }
    }
}

結果こんな感じ。リリースビルドしました。

  1. 00:00:01.0881991sec
  2. 00:00:01.1056796sec
  3. 00:00:01.1177558sec

だいたい 1.10 秒くらい。0.27 秒くらい遅い。しょうがないね、GroupBy 使ってるからね。うん、C と比べて0.27 秒ほど遅いです。

これはもうちょっと早くなるよ。

using System;
using System.Linq;
using System.Runtime.Serialization;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using Utf8Json;

namespace csharp
{
    class Program
    {
        private static readonly string _csvPath = @"C:\Users\****\Documents\work\private\real-test\test.csv";

        static void Main(string[] args)
        {
            var outPath = Path.Combine(Path.GetDirectoryName(_csvPath), "result_csharp.json");
            if (File.Exists(outPath))
            {
                File.Delete(outPath);
            }

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            (string a, int z) LineSelector(string line)
            {
                    var parts = line.Split(",");
                    float x = float.Parse(parts[2]);
                    float y = float.Parse(parts[3]);
                    int z = x > 0
                            ? (int)(x * y + 0.0000001)
                            : (int)(x * y - 0.0000001);
                    return (a: parts[0], z: z);
            }

            string KeySelector((string a, int z) data) => data.a;

            int ValueSelector((string a, int z) data) => data.z;

            var lines = File.ReadLines(_csvPath)
                .Skip(1)
                .AsParallel()
                .Select(LineSelector)
                .GroupSum(KeySelector, ValueSelector);

            var json = JsonSerializer.SerializeUnsafe(lines);
            using (var fileStream = File.OpenWrite(outPath))
            {
                fileStream.Write(json.Array, 0, json.Count);
            }
            Console.WriteLine($"{stopwatch.Elapsed}sec");
        }
    }
    
    public static class EnumerableExtensions
    {
        public static IEnumerable<KeyValuePair<TKey, int>> GroupSum<TSource, TKey>(
            this IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector,
            Func<TSource, int> valueSelector)
        {
            var dic = new Dictionary<TKey, int>();

            foreach (var item in source)
            {
                var key = keySelector(item);
                if (dic.TryGetValue(key, out var x))
                {
                    dic[key] = x + valueSelector(item);
                }
                else
                {
                    dic[key] = valueSelector(item);
                }
            }

            return dic;
        }
    }
}

修正点は以下だよ

  • GroupBy なんてリッチなもの使う必要ないから、GroupSum っていう関数を作ったよ。
  • ラムダ式よりローカル関数のほうが早いよ
  • AsParallel で並列化することで早くできるよ
  1. 00:00:00.9130819sec
  2. 00:00:00.9389940sec
  3. 00:00:00.9559440sec

0.93 秒くらい。 python より 0.10 秒遅いくらいだね。まあそんなもんです。

っていうか C で書かれたライブラリ使っていいなら C# でも C で作った DLL 呼べばいいんじゃないかな。C# のみで(もちろん Utf8Json も純 C# 製)C と競える速度なので、Linq が遅いってのは見当違いです。Linq ってか GroupBy がそんな早くないってだけ。

おまけ


C# より python が早いと言わせる所以

2018/4/25

ほとぼりが冷めてきましたので、後日談というか。

ことの発端としては、引用した記事に対して、僕の方も「C#が遅い」と堂々と書かれてしまのにカチンと来てしまって結構煽りっぽい記事を書いてしまいました。結果的に目を引く記事タイトルになったかもしれないんですが、ちょっと過激だったかなと少し反省。でもまあ、python より C# が遅いなんてことは万に一つもないわけで、遅かれ早かれ誰か反論記事を書いてたんじゃないかな、とは思います。

しかし現実的に考えて、スクリプト言語である python とコンパイル言語の C# を比較して、C# の方が遅いなんてことは、少なくとも今はないんですよね。さらに言うと、.NET Core でかなり中身が最適化されている上に、C# 7系では主にパフォーマンスを改善する改修が進められていて、今では C/C++ と競えるレベルまでパフォーマンスが上がっているわけです。

ではなぜ某氏に python のほうが早い!と言わしめたかというと、某記事にも書いてある通り、当人がプログラムを専門に書く人ではない、というところにある気はします。LINQ が遅いという話にファイルのシリアライズまで含まれているところや、Pickel が早い!という感想から思うに、そもそも言語の理解自体あいまいなのかもしれません。(ちなみに僕も知らなかったんですが、Pickel は python オブジェクトをダンプしたものみたいです。そりゃシリアライズゼロなんだから早い)

で、そういう人からすると、ライブラリが豊富で、しかもその中の挙動が完全に隠蔽されてるような言語が早く見えると思うんですよね。一応某記事にも numpy や pandas の中身が C でごりごりにチューニングされている、といった旨の文章が書かれていますが、そういう意味で、C# にはごりごりにチューニングされた集計ライブラリが見つけづらいのが python を早いといわせる所以でしょうか。

これはたぶん C# の環境自体に問題があって、実際 C# のライブラリは Microsoft が提供する、という図式が成り立ってる気がするんですよね。サーバアプリだったら ASP.NET だとか、データベースアクセスは Entity Framework だとか。最近では Nuget が盛んになってきて、僕もよく Nuget を使って外部ライブラリを使ったりはしますが、Utf8JsonSelenium みたいな定番を除いて Microsoft 以外の Nuget パッケージを利用した記憶があんまりないです。そういう意味で、python のとっつきやすさも相まって、C# は python より不便だと思わせてしまっているのかもしれません。

なのでまあ、C# をとっつきやすい言語にするためにも Nuget をもっと充実していきましょうね、といった感じですかね。新しいものもそうですが、numpy や pandas みたいな強力な集計ライブラリが C# にないっていうのは今のご時世ちょっと大変かもしれません。いくら Linq が早くても半データベース構造してるライブラリには勝てなさそうなので。

Discussion