wiprog

C#, .NET, Scala ... について勉強したことのメモ

読書メモ: 起業の科学 #1 スタートアップにとっての「良いアイデア」とは

いかに課題にフォーカスするか

解決する課題の質を高めよ

「スタートアップの生死を分けるのは、 Product Market Fit (PMF, 市場で顧客から熱狂的に愛される製品のこと) を達成できるかできないかだ」 - Mark Andreessen, Founder of Andreessen Horowitz

いくら優れたプロダクトを生み出しても、市場に受け入れられなければ成長はできない。 PMF を達成するためにはまず、自分たちのビジネスアイデアが市場から求められているものなのかを検証すること。 スタートアップにおいては 課題の質 にフォーカスすることが最も重要である。 スタートアップのアイデアは、 課題ドリブン (課題ありき) ではなく、ソリューションドリブン技術ドリブン であることがあまりに多い。

「バリューのある仕事をしようと思えば、取り組むテーマは イシュー度解の質 が両方高くなければならない」 - 安宅和人, CSO of Yahoo

バリューのあるアイデアを見つけるには、「課題の質を上げてから、ソリューションの質を上げる」というパスしかない。 よって、スタートアップを始めるにあたって真っ先に注力すべきは、解決を目指す課題の質を向上させることである。

  • 今検討しているアイデアは、顧客にとって本当に痛みのある課題なのか?
  • このアイデアの妥当な代替案が、既に市場に存在していないか?

のように、様々な角度からアイデアの深堀りを繰り返していくことで、課題の質が上がる。 それができたら、その課題に対する解決策を検討し、磨きをかけていくことで、初めて価値のある「良いアイデア」に至る。

課題を軽視して大失敗

課題の質ではなく、ソリューションの質にこだわると、顧客にはほとんど使われずに終わる。

グーグルグラスや Apple Watch は膨大な資金力とブランド力を背景に、 PMF ではなく力技を選んだ。 しかし結局、ユーザの期待に応えることはできず、思うようには売れなかった。

課題を意識しないのは自殺行為

先の例が示しているのは、 Google や Apple のような実績や資金力のある彼らですら、課題の質を軽視して、プロダクトありき、ソリューションありきで考えると容易に失敗するということ。 資金も人材も知名度もないスタートアップが課題を軽視することは、ただの自殺行為である。

課題の質を決める 3 つの要素

課題の質は、ファウンダー(創業者)個人が次の 3 つの要素をどれだけ持つかに依存する。

  • 高い専門性
  • 業界(現場)の知識
  • 市場環境の変化 (PEST: Political, Economic, Social, Technological) に対する理解度

これらが極めて重要であり、ファウンダー個人に求められる。

自分ごとの課題を解決せよ

課題の質を高めるもう一つの要素は、ターゲットとする課題が「自分ごと」であるかどうか。 Airbnb (創業者が家賃を払えないという切迫した状況から生まれた) や、ダイソン (従来の紙パックの掃除機の吸い込みの弱さやパックの交換が面倒で大きな憤りを覚えた) は、自分が痛みを感じている具体的な課題から始まり、その課題を自分ならどう解決するかという順番でビジネスアイデアが形成されている。

魔法のランプクエスチョン (もし魔法のランプがあって、課題を解決するためにソリューションを出してくれるとしたら、どんなものがいいか?) から導き出される答えの中に、ダイヤの原石があるはず。 自分ごとの課題といっても必ずしも本人が当事者である必要はなく、周りの身近な人が抱えている課題でも、その課題の痛みをしっかり理解しているのであれば OK。

課題の発見は、まだ「始まり」

課題の発見は始まりにすぎない。重要なのはその後の磨きこみである。 このときに自分ごとの課題でなければ、本気で磨きこみができない。

なぜあなたが、それをするのか?

スタートアップでは、 第三者の課題 (自分がそこまで共感や思い入れがない他人の課題) を解決しようとすることは避けなければならない。 強い共感が持てない課題は自分ごとにならず、痛みの検証がどうしても表面的になり、結果的に真の課題にたどり着くことが困難になる。 また、説得力もないので周囲からの協力も得られにくい。

「YC のインタビューでは『誰がその製品を心の底から欲しがっているのか?』を聞く。ベストの答えは起業家自身であることで、次に良いのは、ターゲットユーザをものすごく理解しているのがわかる解答だ」 - Sam Altman, President of Y Combinator

ファウンダーの課題に人は集まる

課題への強い共感や思いが、スタートアップのビジョン、ミッションに翻訳されていく。 プロダクトを市場に本格投入するまでのフェーズにおいて、ビジョンやミッションはスタートアップの最大の競合優位性になる。 ユーザ、起業メンバー、投資家はいずれもファウンダーが語るビジョンとミッションに惹かれて集まる。

スタートアップが歩む道のりが想像以上につらいことも、自分ごとの課題から始める必要がある理由である。 最初に立てた仮設はほぼ覆ってしまい、限りある資金がどんどん減っていく。 この時、ファウンダーが自分ごとの課題を持ち、将来的にこうあるべきだという強いビジョンやミッションを掲げていれば、高いレジリエンス(メンタルの回復力)の源になる。

課題に出会った原体験は何か

「自分ごとの課題になっているかどうか」を別の言い方で自問するなら、「その課題にストーリー(原体験)があるか」

誰が聞いても良いアイデアは避ける

アイデアは crazy か

誰が聞いても良いと思えるアイデアは、スタートアップにとっては選んではならないアイデアだ。 ネガティブなフィードバックが周囲から集まる状態を「マーケットが定義されていない状態」ととらえて、今のタイミングで事業を手掛けることがチャンスだと考える。

「一見アンセクシーだが、実はセクシーなアイデアを見つけることが決め手となる」 - George Kellerman, COO of Yamaha Motor Ventures & Laboratory Silicon Valley

例: 排せつ予測デバイス D Free

こうした crazy でアンセクシーなアイデアは、人に話すのが恥ずかしいはずだ。 人に話すのが恥ずかしい段階とは、その課題を言語化して説明するフレームワークを入手できていないために、人に伝えるのが困難だということ。 当然、その課題に目をつけている企業はまだ存在しないか、ごくわずかである。

一方、言語化して人に伝えられるような課題をターゲットにした場合は、既に課題が認識されており、妥当な代替案がある場合が多い。 このようなビジネスは投入できるリソースの勝負や価格勝負になるため、スタートアップが狙うのは賢明ではない。

「競争は負け犬がすることだ」 - Peter Thiel, Founder of PayPal

市場のシェアを激しく奪い合う消耗戦になると、リソースの多い大企業が圧倒的に有利になり、スタートアップに勝ち目はない。

大企業の意思決定の仕方

大企業が新規事業を始めるときは、取締役会でほとんどの役員が賛同しないと稟議の承認が下りない。 判断の際に役員が気にするのは課題の質などではなく、その事業がもたらす売り上げや利益の見通し、蓋然性、既存のコアビジネスと競合しないか、といった点である。 そのため、アンセクシーなアイデアには挑戦しにくい。

「スタートアップではハードなことをするほうが実は近道である。簡単な道を選ぶことは結果として遠回りになる」 - Sam Altman, President of Y Combinator, lecture in Stanford University

一見悪いアイデアが世界を変えた

crazy なアイデアで大成功を収めたスタートアップの代表格は Airbnb だ。 犯罪大国のアメリカで赤の他人の家に泊まる、他人を自宅に泊めるという行為は、まさにバッドアイデアそのものだった。

「多くの人が、 Airbnb はうまくいった最悪のビジネスアイデアだと言っている」 - Brian Chesky, CEO of Airbnb

Uber のように、だれもが当たり前だと思ってきたこと(タクシーを捕まえるために何分も手を挙げて待ち、英語が片言のドライバーに目的地を伝えるのも大変で、支払いは基本的に現金)に疑問を投げかけられるかどうかが、スタートアップが世界を変える存在になれるかどうかの最初の分かれ道になる。

他の人が知らない秘密を知っているか?

成功する人は、ほかの人が知らない秘密を知っている。 ここではインスタカート(スマホなどで注文すると近所のスーパーでの買い物を代行してくれるサービス)の例を見ていく。

インスタカートが大成功した理由

インスタカートは店舗と商品を選ぶと、「ショッパー」と呼ばれる一般人が、注文した人の代わりに店まで買い物に行き、 45 分以内に自宅まで届けるという仕組み。 創業者の Apoorva Mehta はアマゾンの物流システムを開発するエンジニアで、物流と小売りに関して高い専門性があり、市場の流れをつかんでいた。 しかし、彼自身が glossary shopping (食料品の買い物) は自分で行うもの、という通例に疑問を持った視点こそが重要。

インスタカートは経歴チェックやトレーニングプログラム、カスタマーレビューの仕組みを導入し、見知らぬ人に直接口委に入れる食料品の買い出しを頼んだり、住所を教えたりすることの不安を払拭した。 このサービスは全米に一気に広がった。

Mehta は Amazon 在職中に新規事業としてインスタカートのアイデアを提案したが、自社ビジネスと競合する奇抜な試みでしかないと却下された。 Amazon が膨大な資金を投資してきたインフラ (自社倉庫、外部の物流会社との契約) を否定することになるからだ。 この却下によって Amazon が現状のシステムでこのサービスに対応できないことが明確になり、Mehta はアマゾンを脅かすほどのビジネスチャンスがあると気付いた。

小売りとの win-win 関係

Amazon の台頭はスーパー等の小売店にとって脅威である(カスタマーの予算を奪い合う)が、インスタカートの場合は Win-Win の関係が成り立つ。これが Mehta が気付いた 秘密 である。

なぜ crazy なアイデアが求められるのか

crazy なアイデアが求められる背景には、 IT の進歩で、マーケットのパラダイムシフトが高速化していることがある。 2014 年ごろから、ユニコーン企業が続々と生まれるようになり、評価額の上昇カーブもすさまじいものになった。 この事実は、プロダクトやサービスの が短くなっていることも意味する。 これだけの速さだと、 First Mover (最初の市場参入会社) が出てきたのを後追いしても遅い場合があり、だからこそ、誰よりも先に PMF を達成することが重要である。 このときにベースになるのが、「一見すると悪いが、本当は良いアイデア」である。

イノベーションカーブの変化

従来のイノベーションカーブは、innovator (革新者) がいて、Early Adopter (新たに登場した商品、サービス、ライフスタイルなどを、比較的早期に受け入れ、それによって他の消費者・ユーザーへ大きな影響を与えるとされる利用者層) がいて、Chasm(深い裂け目、の意。Earyly Adopter と Early Majority の間の大きな溝) を超えるとようやく Early Majority (Early Adopter に追従する形で受容し始める利用者層)に到達する、といった話だった。 このイノベーションカーブであれば、ある程度の時間をかけてプロダクトを検証することができたが、スマホなどの普及による情報伝達スピードの加速で、このモデルは古くなっている。 現在は Trial Customer (新しもの好きの顧客) と Burst Majority (爆発的に広がる一般層) の 2 段階できわめて速く浸透する。 ユーザ数がある値を超えるのを待たず、一定数の Trial Customer のフィードバックをベースにプロダクトを磨きこむという新時代は、スタートアップにとっては好機ともいえる。

代替案のないアイデアを探せ

「新規事業を考えるときには無消費をターゲットにせよ」 - Clayton M. Christensen

無消費とは、顧客が何も持たない状態のこと。つまり、前例もなければ、既存の消費者もいない、代替案のない状態のこと。 こうした場所を発見して PMF を達成できれば、大きな成長を見込める。

ロイヤルティーループの劇的変化

Royalty loop とは、製品を知った人がそれを気に入り、ユーザとして定着して使い続けてくれるまでの流れを輪のような形で示したもの。

従来の Royalty loop は、 AIDMA(Attention - Interest - Desire - Memory - Action) モデルであり、ユーザは一つのプロダクトやサービスを選ぶために時間をかけて、それを使う価値があるかを検証していた。 最近はほとんどのサービスで、「最初の 1 カ月は無料」といったフリーミアムが定番化した。まず使ってみて、有用だと思ったら購入する。気軽に始め、使っていく中で本格的にユーザがそのサービスに定着する (他の代替案を捨てる) というあらたな Royalty loop が生まれた。

また、継続的に使用する顧客との接点が多いフリーミアムやサブスクリプションモデルの利点はユーザの囲い込みだけではなく、フィードバックを高速に集められるという点にもある。 それをもとにプロダクトを絶えず改善していくことで、ファンの量と質 (高い LTV、低い解約率) を増やしていくことができる。

スタートアップが避けるべき 7 つのアイデア

1. 誰が見ても、最初から良いアイデアに見えるもの

一見よさそうに見えるアイデアはすでに誰かが手掛け、たいていは失敗している。

2. ニッチすぎる

あまりにも市場がニッチすぎると、将来的な成長が見込めず、スケールしない。

3. 自分が欲しいものではなく、作れるものを作る

技術ドリブンではいけない。技術的に作れたから作っただけのプロダクトは、課題から生まれたものではないので市場に受け入れられない。

4. 根拠のない想像上の課題

クラウドファンディングなどを使うと、たまたま出したコンセプト動画が受けて必要以上の金額が集まり、後戻りできなくなるケースがある。 Customer Problem Fit を実現しないうちに、表面的に PMF を達成したような状態。 もしビジネスをスケールするつもりなら、どんな課題を解決できるプロダクトになっているかを検証すべき。

5. 分析から生まれたアイデア

一時期あふれかえってどれも失敗したグルーポンの模倣サービスは、市場を俯瞰して空いている部分を狙うというロジカルなトップダウンアプローチでビジネスを展開しただけで、スケールするストーリーや、ファウンダー自身の思いが欠けていた。

6. 激しい競争に切り込むアイデア

スタートアップは「競争を避けること」が戦略の第一義であると考えるべき。

7. 一言では表せないアイデア

「誰 (customer) の何 (課題) をどのように解決するか」を一言で表せないアイデアは磨きこみが足りない。

読書メモ: Peopleware #1 人材を活用する (1) - プロジェクト失敗の原因

トム・デマルコの名著、 Peopleware を読み始めたので自分の意見も交えて読書メモ。 本の要約と自分の意見が半々ぐらいな感じです。ここ数年の話題だったり、主語が「私」となっているものは私個人の意見です。

今日もどこかでトラブルが

筆者は 10 年間にわたり、500 以上の開発プロジェクトとその結果について調査した。 調査の結果、プロジェクトの 15 % が水泡に帰している(中止、延期、納入後に使用されない等)ことがわかり、また、大きなプロジェクトほど失敗する可能性が高いこともわかった。 25 人年以上を注ぎ込んだプロジェクトのうち、実に 25 % ものプロジェクトが完成しなかった。 プロジェクトが失敗した原因の圧倒的多数は、 単なる技術的な問題として片付けられないものばかり だった。

問題の本質

失敗の原因を関係者に尋ねると、異口同音に 政治的要因 と言われたが、この言葉はいい加減に使われていた。 実際には、意思疎通の問題、要因の問題、マネージャーや顧客への幻滅感、意欲の欠如、高い退職率等の 社会学的な問題 であった。 政治的な問題ではなく、プロジェクトとチームの社会学的問題として本質をとらえると、もっと取り組みやすくなるはずだ、というのが筆者の主張。

実際のところ、ソフトウェア開発上の問題の多くは、技術的というより社会学的なものである。

多くのマネージャは実際には技術だけに関心があるというようなマネジメントをしている。 最も大切なのは人間中心に考えることなのに、いつもないがしろにされている(これは企業の教育に原因があるとのこと)。

そういえば今となってはすっかり忘れ去られた 7pay 事件(事故?)のときも、失敗の表面的な原因は技術的にあまりにもお粗末な品質の成果物に見えたが、現場の実態を暴露する twitter での書き込みや、社長の記者会見における発言を見ていると本質はやはり社会学的な問題だったように思う。 普段からコンビニ経営において、一緒に働く仲間をを軽視した本部の姿勢が批判されていることと、この失敗は無関係ではないと思う。

ハイテクの幻影

多少なりとも最新技術に関係している人(これは日本でいうところの『エンジニア』を名乗る人たちに相当すると思う)は、 ハイテクの幻影 に取り憑かれている。 他人の研究成果を応用しているに過ぎないのに、自分がハイテクビジネスの旗手だと錯覚している(日本の twitter や Qiita を見ているとよくわかります)。 ソフトウェアは、多数のチームやプロジェクト、固く結束した作業グループで開発するので、ハイテクビジネスではなく人間関係ビジネスに携わっていると言える。 プロジェクトの成功は関係者の緊密な対人関係によって生まれ、失敗は疎遠な対人関係の結果である。

マネージャが技術的問題にうつつを抜かす理由は、重要だからではなく、単にやりやすいからだ。

ここで、私のような テックリード と呼ばれるポジションの人は特に気をつけたほうが良いと思う。 テックリードは技術的な面でもチームをリードする必要があるが、テックリードになれた人は技術的な面ではすでに強みを持っているはずなので、社会学的な問題のマネジメントに割く労力を意図的に増やしたほうが良い。 これは私も経験済みだが、どうしても取り掛かりやすく仕事をしている気分になれる技術的な問題にフォーカスしがちである。

C# で messagepack vs json 比較

Messagepack の良さを社内に布教するためにベンチマークを取ったので転載しておく。

個人的には MessagePack + LZ4 の Typeless がおすすめ。 messagepack は可読性が・・・と言われることが多いけど、 Typeless なシリアライズなら型情報がつくので、 dynamic にデシリアライズして json を吐くような小さいツールさえ作っておけば gzip 圧縮した json とそこまで使い勝手は変わらないのでは?と思う。 パフォーマンスの面では messagepack が有利。json + gzip はファイルサイズは小さくなったけど、パフォーマンスが許容できるかどうか。

ベンチマークに使ったコードは こちら

ファイルサイズ

json:                     137,248,217 bytes
json + gzip:              045,170,640 bytes
msgpack:                  084,959,431 bytes
msgpack + lz4:            061,381,468 bytes
msgpack (typeless):       083,949,384 bytes
msgpack (typeless) + lz4: 060,776,211 bytes

パフォーマンス

BenchmarkDotNet=v0.12.0, OS=ubuntu 19.04
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.0.100
  [Host]   : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  ShortRun : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  
|                 Method |       Mean |     Error |   StdDev | Ratio | RatioSD |     Gen 0 |     Gen 1 |     Gen 2 | Allocated | Completed Work Items | Lock Contentions |
|----------------------- |-----------:|----------:|---------:|------:|--------:|----------:|----------:|----------:|----------:|---------------------:|-----------------:|
|       Utf8JsonWithType | 1,001.4 ms | 337.02 ms | 18.47 ms |  1.00 |    0.00 | 1000.0000 | 1000.0000 |         - | 642.99 MB |               3.0000 |                - |
|     Utf8JsonWithTypeGz | 4,134.0 ms | 987.83 ms | 54.15 ms |  4.13 |    0.13 | 7000.0000 | 7000.0000 | 2000.0000 | 814.09 MB |               3.0000 |                - |
|       Utf8JsonTypeless |   984.2 ms |  50.38 ms |  2.76 ms |  0.98 |    0.02 | 1000.0000 | 1000.0000 |         - | 642.99 MB |               2.0000 |                - |
|     Utf8JsonTypelessGz | 4,194.6 ms | 777.21 ms | 42.60 ms |  4.19 |    0.04 | 7000.0000 | 7000.0000 | 2000.0000 | 814.08 MB |               3.0000 |                - |
|    MessagePackWithType |   390.3 ms | 231.84 ms | 12.71 ms |  0.39 |    0.02 |         - |         - |         - |  336.9 MB |               2.0000 |                - |
| MessagePackWithTypeLz4 |   537.4 ms |  66.94 ms |  3.67 ms |  0.54 |    0.01 | 2000.0000 | 2000.0000 |         - | 395.79 MB |               2.0000 |                - |
|    MessagePackTypeless |   368.1 ms |   2.59 ms |  0.14 ms |  0.37 |    0.01 |         - |         - |         - | 335.94 MB |               2.0000 |                - |
| MessagePackTypelessLz4 |   515.2 ms |  75.83 ms |  4.16 ms |  0.51 |    0.01 | 2000.0000 | 2000.0000 |         - | 394.22 MB |               2.0000 |                - |
// * Legends *
  Mean                 : Arithmetic mean of all measurements
  Error                : Half of 99.9% confidence interval
  StdDev               : Standard deviation of all measurements
  Ratio                : Mean of the ratio distribution ([Current]/[Baseline])
  RatioSD              : Standard deviation of the ratio distribution ([Current]/[Baseline])
  Gen 0                : GC Generation 0 collects per 1000 operations
  Gen 1                : GC Generation 1 collects per 1000 operations
  Gen 2                : GC Generation 2 collects per 1000 operations
  Allocated            : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  Completed Work Items : The number of work items that have been processed in ThreadPool (per single operation)
  Lock Contentions     : The number of times there was contention upon trying to take a Monitor's lock (per single operation)
  1 ms                 : 1 Millisecond (0.001 sec)

今からできる、速くシンプルに LINQ を書くためのコツ 3 個

たびたび 「LINQ が遅い」 と言われているのを見かけるので、どうやったら速く書けるのか、どう書くと遅くなるのかについてまとめてみます。
LINQ は非常に強力で、ぼくが出会った「LINQ 遅い」のほとんどは、 実装の仕方がまずいものばかりです。
LINQ はいくつかポイントを意識するだけでかなり安全に速く書けるようになります。
いろいろとポイントはありますが、 3 つだけに絞って書いてみました。

私自身 C# 書き始めてようやく 2 年たったぐらいなので、間違っているところはバシバシご指摘いただけると嬉しいです。

こちらの記事も参考になるので、合わせてご覧ください。

blog.okazuki.jp

「LINQ 遅い」の 3 パターン

まあ LINQ は速くないこともたまにあるのですが、大抵の場合は十分なパフォーマンスを提供してくれます。
「LINQ が遅い」 と言う時はだいたい以下のどれかかなと思います。

  1. よくわからないけどイメージで「遅そう」と言っている (要するに使いたくない、覚えたくない)
  2. LINQ のしくみがよくわからず、誤った使い方、明らかに遅い書き方をしている (例: なんとなく Count(), ToArray() などしてしまう)
  3. LINQ の内部実装をよく知っているプロフェッショナルが LINQ が適していない場合に「遅い」と言う

1 の場合はどうにもできないです。 3 の場合は必要に応じて LINQ 以外の方法を取ることもできますし、 LINQ を使うと判断した場合は最も適切なメソッドを選んで使用できるので遅くなりません。プログラマとしてはここを目指したい。
問題は 2 の場合で、「なんとなく動くものは書けるけど遅い、どこが遅いのかよくわからない」というのが多いです。
今回は最も多いと思われる 2 のパターンをターゲットに書いていきます。

1. 不用意に要素数を取得しない (Count メソッド)

一番良く見かけるのがこれです。簡単に書けてしまいますが非常に危険です。

IEnumerable<Hoge> source = // ...

if (source.Count() > 0)
{
    // 要素があるときの処理
}

IEnumerable<T> はカウントを持ちません。いくつ要素があるのかも全部列挙してみないとわかりません。 もしかしたら非常に長かったり、列挙におおきなコストがかかったり、無限につづくシーケンスでそもそもカウントできないかもしれません。
Count メソッドは基本的には 100 万個要素があったら 100 万個全部を 1 個ずつ列挙して数えていくため非常に遅く、ほとんどの場合に意図していない列挙を発生させます。
もちろん Count の前に SelectWhere をかけていて、列挙の際になんらかの計算が発生するような場合にはその計算のコストもかかります。 実体が Count プロパティをもつコレクションである場合には Count プロパティの値を取得するような最適化は入っているものの、本当に「何個あるか知りたい時」以外使うべきでないです。

source.Count() > 0 と書くのであれば、代わりに source.Any() と書きましょう。

「ある条件を満たす要素が n 個以上あるかどうかを判定したいとき」には下記のように Skip や Take を使用することで、一部だけの列挙におさえることができます。

var filtered = source.Where(predicateFunc);

// 列挙した要素を一切使わず、ただ n 個以上あることをたしかめたいとき
// 列挙したものを保存する必要がないのであれば、この方法で無駄な配列確保を避けられる
if (filtered.Skip(n - 1).Any())
{
    // n 個あった時の処理
}

// 列挙した要素を n 個使用するとき
// 複数回同じものに対しての列挙を避けるには、ToArray() が有効。
// ただし、 n が大きくなった場合、それだけ大きい配列が確保されるので注意。これは ToList() でも同様
var part = filtered.Take(n).ToArray();

if (part.Length == n)
{
    foreach (var item in part)
    {
        // なにか処理
    }
}

2. なんとなく配列やリストに突っ込まない。ライブラリを作るときはなるべく IEnumerable<T> で受ける。

ToArray()ToList() は非常に便利なメソッドですが、ほとんどの場合 LINQ の途中で呼ぶ必要はありません。 たとえば、下記のような ToArray は意味がないばかりか、無駄な配列のためのメモリを確保してパフォーマンスを著しく低下させます。

IEnumerable<Hoge> source = // ...

var array = source.ToArray() // むだな配列生成
    .Select(x => ごにょごにょ)
    .Where(x => ごにょごにょ)
    .Distinct()
    .ToArray(); // むだな配列生成

// foreach するだけならむだな array はいらない
foreach (var x in array)
{
    // なにかする
}

下記のように書いてもまったく動作上問題がなく、パフォーマンスがよくなります。

IEnumerable<Hoge> source = // ...

var array = source
    .Select(x => ごにょごにょ)
    .Where(x => ごにょごにょ)
    .Distinct();

// foreach するだけならむだな array はいらない
foreach (var x in array)
{
    // なにかする
}

また、配列である必要がないのに配列で引数を要求するメソッドを書くのはやめましょう。 IEnumerable<T> で受けましょう。 本当は 1 個ずつ処理するだけのメソッドなのに、渡す側で ToArray() して渡す必要があるのは無駄です。

3. 巨大なファイルを 1 行ずつ処理するときも、リストはいらない。

たとえば、こんなコードを書いたことはありませんか?これはほんとうに無駄なのでやめましょう。

// 1 行ずつ処理したいけど、 LINQ つかうから IEnumerable<T> がほしい。 List<T> に Add していこう

List<Hoge> sourceList = new List<Hoge>();

using (var reader = new StreamReader(stream))
{
    string line;
    while ((line = reader.ReadLine()) != null
    {
        sourceList.Add(MapToHoge(line));
    }
}

sourceList.Select(xxx).Where(xxx). // ...

こんなふうに書けば、一気に全部読んでしまう必要はまったくありません。

IEnumerable<Hoge> Read()
{
    using (var reader = new StreamReader(stream))
    {
        string line;
        while ((line = reader.ReadLine()) != null
        {
            yield return MapToHoge(line);
        }
    }
}

Read().Select(xxx).Where(xxx) // ...

あるいは、ファイルから読むことがわかっているならこれでも良いです。

File.ReadLines("filepath")
    .Select(xxx)
    .Where(xxx)
    . // ...

File.ReadAllLines メソッドもありますが、こちらはすべてを読んで配列に入れてから返してくるので気をつけましょう。巨大なファイルを読む場合に大量のメモリが必要になります。

まとめ

たくさん書きましたが、要するにおなじシーケンスに対しての複数回の列挙や、必要のないものの列挙、巨大なメモリ確保にもっと慎重になりましょうということです。
LINQ は遅延評価が基本ですが、誤った使い方をすればそのメリットを活かせないばかりか、非常に遅いコードが簡単に出来上がってしまいます。
遅いのは LINQ のバグでも、 .NET Core 開発チームの怠慢でも、マシンのスペックが足りないからでもなく、ただ遅くなるように書いたからです。そうでないこともありますが、だいたいそうです。

自分で書いたコードが遅かったり、すっきり書けなかったりして困ったときに、Qiita や twitter に投稿するといろんな人のアドバイスが受けられて楽しいです。
読む人にとってはその投稿についたコメントや、反応の記事のほうが役にたつことがたくさんありますが、最初の投稿がなければそれらの記事も生まれません。

LINQ の内部の実装を読むことは非常に勉強になります。
GitHub で .NET Core の完全な実装を読むことができます。
難しく見えるかもしれませんが、単純なもの、きになるものから読んでいくといいと思います。 また、簡単そうなメソッドを自分で書いてみるのも理解するには効果がありました。
LINQ を書く際に気をつけることはこれがすべてではありませんが、ドキュメントやコードをきちんと読んだり、信頼できる先輩にアドバイスをもらったりしながらだんだんと身についていくものだと思います。

github.com

あと、 ReSharper などのツールを入れると multiple enumeration の注意を出してくれたりもします。

www.jetbrains.com

(ReSharper は他にもいろいろなことを教えてくれる素晴らしい先生になるのでめちゃくちゃおすすめです)

(おまけ) 記事を書いたきっかけ

もともと微妙なコードをちょくちょく見かけていて、 LINQ ってそんなに難しいのかなーと思っていたのですが、 twitter で
C#でLinqを使うよりPythonの方が2倍速かったのでベンチマークをしてみた - Qiita
という記事が回ってきました。

公開から短時間でいいねが複数ついていたことから、ほんとうは速くシンプルに書けるにもかかわらず、 LINQ が遅いと判断してしまう人は多いのではないかと思い、 急いで記事を書いている次第です (そもそもこのブログ自体そんなに見られてないのでどの程度効果があるかは疑問ですが)。

この記事の具体的な内容についてはもう十分他の方が指摘などされていると思いますので、ここでは細かく言及しません。 ただ、もし初心者の方が読まれる場合には下記の点だけは頭においてほしいです。

  1. (意図的にそうしているとは思いませんが、結果として) 検証用のコードが非常に遅くなるように書かれています。 検証用のコードを改善することでだいぶ速い結果になる、という記事がすでにあります。GroupBy が遅いように書かれていますが、それよりも 無駄な ToList 何度も呼んでいるめにループの回数が増えてしまっていることのほうがはるかに大きな問題です。C# の Linq が python の2倍遅い、は嘘 - Qiita
  2. Span<T> に関しては用途が違うように思います。 Qiita の記事とは全く無関係ですが、 日本語で読める Span<T> の記事としては Span<T>構造体 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C がとても参考になります。
  3. 実際には、LINQ に限らず .NET のパフォーマンスの改善や新機能の開発に多くの優秀なエンジニアが取り組んでいます。また、明らかに遅かったり問題があり、改善方法があることが本当にわかっているなら、 issue をあげたり、プルリクエストを送ってみたりする手段は誰にでも開かれています。 GitHub - dotnet/corefx: This repo contains the .NET Core foundational libraries, called CoreFX. It includes classes for collections, file systems, console, XML, async and many others. We welcome contributions.

BenchmarkDotnet を使って LINQ の部分のベンチマークをとってみたところ、標準の LINQ メソッドだけで 3 倍以上高速になりました。

SlowLinq が Qiita の元記事、 NormalLinq がそれを書き直したもの、 UseGroupSum が GroupBy を使わないものです。

Method Mean Error Gen 0 Gen 1 Gen 2 Allocated
SlowLinq 321.59 ms NA 12400.0000 6200.0000 800.0000 78916.81 KB
NormalLinq 85.77 ms NA 1400.0000 600.0000 - 8415.32 KB
UseGroupSum 62.12 ms NA - - - 147.43 KB

Utf8Json で JSONP を書く

Utf8Json.JSONP というライブラリを作りました。 ざっくりしたライブラリの紹介と、どうやって実装したかのメモです。

簡単な紹介

nuget でインストールできます。

NuGet Gallery | Utf8Json.Jsonp 1.1.0

nuget パッケージを入れると、 Utf8Json の名前空間Jsonp というクラスが追加されます。
これが JSONP 版の JsonSerializer です。

使いごこちは JsonSerializer と同じになるようにしてあります。 QuickStart にも載せたのですが、こんな感じでかけます。

var p = new Person { Age = 99, Name = "foobar" };
var callback = "callbackFunc";

// result: callbackFunc({"Age":99,"Name":"foobar"});

// obj -> byte[]
byte[] bytes = Jsonp.Serialize(callback, p);

// write to stream
using (var stream = new MemoryStream())
{
    Jsonp.Serialize(stream, callback, p);
}

ソースはこちら。

github.com

対応してないもの

とりあえず仕事ですぐに使いたくて週末にざざっと作ったので最低限の機能にしています。
落ち着いたら対応していく予定です。

  • 型なしのシリアライズにまだ対応していません
  • ASP.NET Core への組み込みもまだです(仕事では webforms だったので。。。。。。。)

作った目的

ちょうど仕事で Utf8Json への置換えを進めていて、 JSONP に対応してる API どうしよう・・・って考えたのがきっかけです。 せっかく Utf8Json で超高速な JSON シリアライズができるのに、 JSONP のために文字列とか byte 配列の結合はやりたくなかった。

最初の実装

まず Utf8Json 置き換え前のコードはこんな感じでした。 (Utf8Json に比べると) すごく遅そう。

string json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
string jsonp = string.Format("{0}({1})", callback, obj);

で、 Utf8Json に置き換えて最初にやった実装がだいたいこんなかんじ。

// json と callback の文字列をそれぞれ byte[] に変換
byte[] jsonBytes = Utf8Json.JsonSerializer.Serialize(obj);
byte[] callbackBytes = Encoding.UTF8.GetBytes(callback);

// 結果の byte[] を作る
int byteCount = jsonBytes.Length + callbackBytes.Length + 2;
var jsonpBytes = new byte[byteCount];

// コピーしていく
Array.Copy(callbackBytes, 0, jsonpBytes, 0, callbackBytes.Length);
jsonpBytes[callbackBytes.Length] = (byte) '(';
Array.Copy(jsonBytes, 0, jsonpBytes, callbackBytes.Length + 1, jsonBytes.Length);
jsonpBytes[byteCount - 1] = (byte) ')';

Utf8Json を使っているのでなんとなく速そうに見えますが、 byte[] を 3 つもつくっています。そしてこのあとさらに stream に書き込んだりするのです。これは遅い。
utf8json の SerializeUnsafe() を使えば多少は良くなりそうですがそれにしても完璧ではなさそうです。

Utf8Json の primitive API

配列のコピーをなくして、そのまま JSONP の形になっている byte[] がとれればいちばんよいです。
で、それをやるには Primitive API として公開されている JsonWriter を使って、 シリアライザが内部で持っているバッファに直接書き込んじゃうのが簡単そうでした。

単純にやればとても簡単で、まずやったのはこんな実装でした。 v1.0.0 の実装はこれです。

public static byte[] Serialize<T>(string callback, T value, IJsonFormatterResolver resolver)
{
    // MemoryPool, BufferPool は自分で用意する必要があった
    var writer = new JsonWriter(MemoryPool.GetBuffer());
    
    // callback の文字列と ( を書く
    writer.WriteRaw(Encoding.UTF8.GetBytes(callback));
    writer.WriteRaw((byte) '(');

    // Utf8Json の API をつかって json を書く
    Utf8Json.JsonSerializer.Serialize(ref writer, value, resolver);

    // ) を書く
    writer.WriteRaw((byte) ')');

    return writer.ToUtf8ByteArray();
}

callback 文字列のシリアライズでの配列確保を避ける

これで満足していたのですが、さっきのコードのこの部分

// callback の文字列と ( を書く
writer.WriteRaw(Encoding.UTF8.GetBytes(callback));

これが気になりはじめました。
これだと callbackbyte[] に変換するためだけに新しく配列を作ってしまいますし、それを JsonWriter 内部の配列にコピーしないといけません。
最初の実装とそこまで違うの?という感じです。

で、 GetBytes() には任意の配列に書けるオーバーロードがあるのでそれを使ってこんなふうにしました。 すこし無理やりな実装ですが、これで配列が作られるのを防ぎます。

public static byte[] Serialize<T>(string callback, T value, IJsonFormatterResolver resolver)
{
    // MemoryPool, BufferPool は自分で用意する必要があった
    var writer = new JsonWriter(MemoryPool.GetBuffer());
    
    // writer の内部バッファを取得する
    ArraySegment<byte> buffer = writer.GetBuffer();
    
    // バッファに callback の文字列を書いて、その分だけバッファの現在位置を進めてあげる
    // ここがずれると大変なので、渡すのが byte[] でいい時は WriteRaw() とかをつかうのが良さそう。
    var filledCount = Encoding.UTF8.GetBytes(callback, 0, callback.Length, buffer.Array, buffer.Count);
    writer.AdvanceOffset(filledCount);

    writer.WriteRaw((byte) '(');

    // ( を書く
    writer.WriteRaw((byte) '(');

    // Utf8Json の API をつかって json を書く
    Utf8Json.JsonSerializer.Serialize(ref writer, value, resolver);

    // ) を書く
    writer.WriteRaw((byte) ')');

    return writer.ToUtf8ByteArray();
}

簡単なベンチマークをとってみたらかなり速くなってました。

Method Mean Error StdDev Gen 0 Allocated
OldVersion 205.1 ns 3.017 ns 3.816 ns 0.0608 96 B
NewVersion 159.0 ns 2.347 ns 2.196 ns 0.0405 64 B

おまけ: SerializeUnsafe について

Utf8Json の APISerializeUnsafe() というメソッドがあります。 (もちろん Jsonp でもサポートしてます)
このメソッドは何が unsafe かというと、スレッドごとに一個しかないシリアライズ用のバッファをそのまま返してきます。
つまり、結果を受け取ったらすぐに使ってしまわないと、他のオブジェクトをシリアライズした場合に書き換えられてしまう可能性があるということです。
unsafe でない API はこれを避けるために、新しく byte[] を作って返してくれます。

どんなときに使うと良いか

たとえば結果をすぐに使って捨ててしまう場合 (たとえば stream に書くとか) の場合は SerializeUnsafe() をつかったほうがお得です。
逆に結果を引き回したり、スレッドをまたいで共有したり、複数のオブジェクトをシリアライズしてあとでまとめて何かする、という時には使えません。

実際、今回も stream に書き込みをする API はこんな感じで実装していますが、これで安全に速く動きます。
この API は本家の Utf8Json にもあるものなので、stream に書いてしまう場合はこれを呼んでもらえれば大丈夫だと思います。

public static void Serialize<T>(Stream stream, string callback, T value, IJsonFormatterResolver resolver)
{
    var buffer = SerializeUnsafe(callback, value, resolver);
    stream.Write(buffer.Array, buffer.Offset, buffer.Count);
}