その処理、本当に遅いですか? 〜無駄を削る達人になろう〜
その処理、本当に遅いですか?〜無駄を削る達人になろう〜HRテック事業部 桐生 直輝2023/06/07
View Slide
自己紹介桐生直輝 (23歳)● 入社:2023年3月● 出身:岡山県津山市● 所属:HRテック事業部○ バックエンド〜インフラ担当● 趣味:コンピュータいじり、自宅サーバー構築● 目標は技術力でとことん尖ることです
今回のテーマ「動作速度」と「高速化」について● 動作速度はシステム品質の重要な柱(のはず・・・)● コード品質の話は出てきません
本セッションの目的以下のことをお話します● 処理を速くするためにどういった流れで手を付けていくか● 各場面で抑えるべきポイント少しでも処理の高速化を上手にできるようになる!
処理高速化のアプローチ
処理高速化のアプローチ1. 「無駄」のニオイを感じ取る2. 処理の遅い原因を特定する(計測)3. 高速化のための手段を考え、実装する4. 実装した高速化の効果を測定する
「無駄」を感じ取るには「無駄」のある処理とは?● 遅いからといって、全ての処理を高速化できるわけではない○ リソースを完全に使い切っている、本質的に重い処理○ 無駄を多分に含んでいる処理● 既に高速化の余地がない処理にいくら労力を費やしても意味がない
「無駄」を感じ取るには「無駄」のあるなしを見分けるには?● 想定される処理時間と比較して見抜く○ 実際の処理時間が想定より明らかに長い =無駄を含んでいる可能性が高い● 「これくらいかかりそう」を精度良く予測できるようにする○ 処理の性質ごとにどれくらいの時間がかかるか、感覚を掴む
「無駄」を感じ取るには何をするのにどれくらいの時間がかかるか、感覚を掴む● 例えば、現代のCPUがどのくらい高速か知っているでしょうか?
「無駄」を感じ取るには何をするのにどれくらいの時間がかかるか、感覚を掴む● 例えば、現代のCPUがどのくらい高速か知っているでしょうか?例:CPU vs ストレージ vs ネットワークアクセス 1秒あたりの処理能力比較 (目安)3,000,000,000 IPS1,000,000 IOPS1,000 RPS1回のネットワーク通信の完了を待つ間に、SSDは1000回の読み書きを実行でき、CPUは300万回の計算を実行できる。※並列度=1
「無駄」を感じ取るには処理の性質ごとに重みが大きく異なることに注意!● 計算処理が中心の場合、相応の時間がかかるのは何百万回〜何千万回もの計算を要する処理であるはず○ 大規模な文字列・データ処理や、一般的な画像処理など○ 業務アプリケーションでそのレベルの計算量を求められることは少ない● 逆に、ストレージ・ネットワークアクセスが中心の場合、比較的小規模でもそれなりに時間がかかることは全然あり得る
「無駄」を感じ取るには処理時間を概算してみる● 例:100ユーザーで、各ユーザー1000件程度のデータを集計する(合計・平均など)○ トータルの処理件数は 100 * 1000 = 10万件程度○ 単純な集計なら1件あたり数十ステップで処理可能 →実時間では数ミリ秒程度○ DBからのデータの取得も 10万件程度なら最大で数秒程度しかかからないはず● ここで概算した時間(数秒)を大きく上回る場合、無駄が存在することになる
「無駄」を感じ取るには結論:処理時間の予測精度を上げる!● 「これそんなに遅くある必要ないのでは?」と思えることが大事● センスを磨くには・・・○ 計算量について学ぶ○ より細かい内部の仕組みを学ぶ
高速化の第一歩高速化できそうな処理は見つけた。次にやるべきことは?
計測
計測せよ「これが原因で遅い」という証拠を手に入れる● 何が原因で遅くなっているかは、計測してみなければわからない● さっきのステップでわかったのは「本来は遅いはずがない」ということだけ
ロブ・パイクのプログラミング5カ条ロバート・C・パイク引用元:https://www.flickr.com/photos/shockeyk/4833152910/in/photostream/「Goの父」と呼ばれる人物ベル研究所でUNIX開発に携わり、Cプログラミングに精通
ロブ・パイクのプログラミング5カ条ルール1プログラムがどこで時間を消費することになるか知ることはできない。ボトルネックは驚くべき箇所で起こるものである。したがって、どこがボトルネックなのかをはっきりさせるまでは、推測を行ったり、スピードハックをしてはならない。引用元: http://www.lysator.liu.se/c/pikestyle.htmlルール2計測すべし。計測するまでは速度のための調整をしてはならない。コードの一部が残りを圧倒しないのであれば、なおさらである。
推測の罠処理が遅い原因の特定を「推測」のみに頼ると失敗する● 例えば、1000件のデータを変換する処理に時間がかかっていたとき1000件もデータがある。きっと変換の計算処理に時間がかかってるんだな処理タイムライン(想像)データ読込変換処理(計算) 結果の保存
推測の罠処理が遅い原因の特定を「推測」のみに頼ると失敗する● 実際は・・・処理タイムライン(実際)データ読込変換処理結果の保存↑ここを改善しないと意味がない!
推測の罠でもこれ、あるあるです● 真面目に計測すると面倒くさいので、つい推測で済ませてしまいがち● 結果、時間のかかってないところを高速化→ほとんど成果が得られなかったりとか
計測の手法代表的なもの● 経過時間を計測し、ログ出力● プロファイラの活用
計測の手法経過時間を計測し、ログ出力● ポイントごとの時刻を記録し、その差分から処理時間を算出するfunction run() {const startTime = new Date(); // new Date()は現在の時刻を取得するconst data = getData();const afterGetDataTime = new Date();const transformed = transformData(data);const afterTransformTime = new Date();saveData(transformed);const afterSaveDataTime = new Date();console.log(‘getData():’, afterGetDataTime - startTime, ‘ms’);console.log(‘transformData():’, afterTransformTime - afterGetDataTime, ‘ms’);console.log(‘saveData():’, afterSaveDataTime - afterTransformTime, ‘ms’);}
計測の手法経過時間を計測し、ログ出力● ポイントごとの時刻を記録し、その差分から処理時間を算出するgetData(): 105 mstransformData(): 92mssaveData(): 4870msコンソール出力
計測の手法プロファイラの活用● 各関数にかかっている処理時間を可視化できるツール● 各ポイントでの時刻の記録を関数呼び出し時に自動的に行うことで実現● プロファイラの例○ ChromeのPerformanceタブ / Node.jsの標準プロファイラ (--inspect)○ PHPのxhprof○ Datadog APMのContinuous Profiler
計測の手法Node.jsプロファイラの例● どこの処理にどのくらいの時間を使ったかが可視化可能
計測の手法プロファイラの注意点● 非同期処理を追うのは難しい(Node.jsの場合)● 関数呼び出しに一定のオーバーヘッドが課されるようになる○ 何万〜何十万回と呼ばれる関数は実際よりも処理時間が多く出てしまうことも
計測の手法まとめ● 一度も計測せずに憶測だけで高速化に着手するのは危険● 一方、計測も完全ではないので、範囲をある程度絞るくらいで良い○ 真因に関しては高速化を実施する中で判明していくことも多い
高速化の実施高速化の普遍的な手法は存在しない● 遅い原因は処理によって千差万別● その対策も状況に合わせて泥臭くやっていく必要がある● 今回は自分が関わったいくつかの高速化事例をご紹介
高速化の実施例:月次の勤怠締め操作 (給与計算ソフト)● 1ヶ月分の勤怠データを締めて給与額を算出する処理● 300人分のデータで7分以上かかる● 処理量が多いためであると諦められていたが、計算内容と処理時間が乖離していると感じ高速化に着手○ 300人 * 30日 = 9000個の勤怠データの集計 + 過去の有給使用データ (300人 * 数十件 = 数千〜1万件) の集計○ データの取得等含めて考えても何十秒もかかるのですらおかしい
高速化の実施例:月次の勤怠締め操作 (給与計算ソフト)● ログを吐いて計測した結果、DBのデータ読み書きに大半の時間を費やしていた● 詳しくコードを調査した結果、以下の2つの原因が判明○ 特定のテーブル(大きめ)にインデックスが張られていなかった○ 1ユーザーの処理ごとに多数のクエリを発行していた
高速化の実施テーブルにインデックスが張られていなかった● ただの凡ミス● 取得に使うカラムにインデックスを張って対応補足:データベースのインデックスとは● 特定の属性(ユーザーID等)に基づいてデータの索引を作る機能● レコードの検索処理の計算量が O(n) から O(log n)に減少する(巨大なテーブルだと、ほぼ定数時間)インデックスに使うB-treeの構造
高速化の実施1ユーザーごとに多数のクエリを発行していた● ループ内でクエリを発行していた&同じデータを何回も取ってきていた○ 何万回ものクエリ発行○ ネットワーク通信待ち + クエリ実行のオーバーヘッドで結構時間を取られる● 全ユーザーのデータを予め一括で取得するようにして解決○ 1件SELECTを10000回やるより10000件SELECTを1回の方が圧倒的に速い
高速化の実施結果● 計算処理には一切手を入れなかったが、10秒程度まで短縮
高速化の実施例2:TypeORM マイグレーションの高速化● マイグレーション = DBスキーマ変更クエリの実行● マイグレーションコマンドが立ち上がるまでに10分弱かかっていた● どう考えても時間のかかり方が異常で、無駄がありそう○ ただマイグレーションのクエリ実行を準備するのに 10分もかからないはずなので
高速化の実施驚きの計測結果● 初期化処理のほとんどはTypeScriptのコンパイル処理○ ts-nodeで動的にコンパイルを行っていたためTypeScriptのコンパイル&ロード処理マイグレーション処理
高速化の実施TypeScriptのコンパイルがそんなに重いはずがない● 確かにtsファイル数は多い(2000ファイル弱)● しかし、プロジェクトのビルド(npm run build)には10秒くらいしかかからない● 仮説: ts-nodeが1ファイルずつビルドしているのでオーバーヘッドが大きく、重い?○ 同じファイルを何度もコンパイルしている可能性?
高速化の実施解決策:事前に全てコンパイル● マイグレーションに必要なtsファイルのみを事前コンパイル(一瞬)● 初期化の時間がほぼなくなり、すぐにマイグレーションが走るように● 必要時間は9分→数秒に改善
高速化の実施まとめ● 基本的には状況に合わせて愚直に無駄を削るしかない
高速化の効果を確認する高速化で処理時間を短縮できた!めでたしめでたし● ・・・本当でしょうか?● 狙った通りに高速化できているか、立ち返って確認しましょう
ロブ・パイクのプログラミング5カ条ルール3凝ったアルゴリズムはnが小さいときには遅く、nはしばしば小さい。凝ったアルゴリズムは大きな定数を持っている。nが頻繁に大きくなることがわかっていないなら、凝ってはいけない引用元: http://www.lysator.liu.se/c/pikestyle.htmlルール4凝ったアルゴリズムはシンプルなそれよりバグを含みやすく、実装するのも難しい。シンプルなアルゴリズムとシンプルなデータ構造を使うべし。
高速化の効果を確認する高速化は実装の複雑化とのトレードオフ● 実装の複雑さに見合うほどの効果が得られたか● 高速化したと思ったのに逆に遅くしてしまっていないか(?!)○ 複数の高速化策を同時に適用したが、実際には一部が逆効果だったというパターン
高速化の効果を確認する実例:キャッシュ入れて高速化したと思ったら遅くなってた件● Jest(Node.js テストフレームワーク)によるテスト実行が遅かった(何十分も要する)● 例によってTypeScriptの動的コンパイルを行っていた● Jestのコンパイルキャッシュ機能を有効化して高速化した気になっていたが・・・
高速化の効果を確認する実例:キャッシュ入れて高速化したと思ったら遅くなってた件● 本件ではそもそもTypeScriptの動的コンパイルの占める処理時間は小さかった○ そもそもts-node自身がコンパイル結果をメモリに保持するのでほぼ効果なし● 逆に、キャッシュが最新のソースを反映しているかの確認処理の方が重かった● 結果、キャッシュ導入前よりも実行時間が伸びる
高速化の効果を確認する敗因● 時間がかかっているのがどこかきちんと計測していなかった○ ちなみに遅くなっている原因は全く別の場所だった● 複数の高速化を同時に行っており、個々の策についての効果を測定していなかった→ 測定した上で、効果のあるものだけを残すべき
高速化の効果を確認するまとめ● 高速化技法が本当に効果をもたらすかは、測ってみなければわからない● 意味のない実装はきちんとコードベースから排除する
まとめ
まとめ● 処理に潜む無駄を見抜けるようになる○ 低レベルな部分まで興味を持つ!● 当てずっぽうで高速化を試すのはやめる● 高速化の効果と釣り合わない実装は排除する
ご清聴ありがとうございました!