高速化におけるメンタルモデル

## はじめに
どうも、後輩に「効率厨」と言われ白米がおいしいです。
自分ではそこまで効率厨とは思っていませんが、
高速化を行う際に考えている事をメンタルモデル化し、抽象的に言語化してみます。

対象者は高速化の入口に立った人です。
何かの一助になれば幸いです。

## 処理時間とは
まず手始めにググってみましょう。

> プログラミングにおける処理時間とは、実行するプログラムが何秒程度かかるのかを計測した時間です。プログラムの処理時間を計測するには、実行するプログラムの直前と直後の時刻を取得し、その差を算出します。引用元：Search Labs | AI による概要

では、この処理時間について順に分解し、処理速度に影響を与えやすい変数を書き出していきましょう。

### 計測段階
最初に計測するならこういう捉え方となります。
何かしらの処理を実行し、終了するまでの時間です。

```mermaid
sequenceDiagram
    participant T as 処理時間(T)
    participant A as Process A
    
    T->>+A: ping!
    A->>-T: pong!
```

#### 処理時間(T)
この変数が今回で短縮する対象となります。

### もう少し掘り下げる

プログラミングでは、何かしら呼び出す処理(API)を実行すると思います。
呼び出し先として抽象化されたProcessBを追加してみましょう。

ProcessBを呼び出すには、一連の操作として下記となります。
1. 引数を作成
2. 呼び出し
3. 結果を得る

図にして変数を加えるとこうなります。
```mermaid
sequenceDiagram
    participant T as 処理時間(T)
    participant A as Process A
    participant B as Process B
    
    T->>+A: ping!
    A-->>A: 引数作成処理(Parg)
    A->>+B: call(argN)
    Note over A,B: 引数サイズ(Sarg)→
    Note right of B: ↓処理時間(Tm)
    B->>-A: resN
    Note over A,B: ←結果サイズ(Sres)
    A-->>A: 結果解析処理(Pres)
    A->>-T: pong!
```
急に変数が増えたので、それぞれ説明を行っていきます。

#### 引数作成処理(Parg)
呼び出し先の処理に必要な情報を集めて、それを適切な形式に整える過程です。
ここで考慮すべきことは、引数の準備にかかる時間と、準備の効率化です。

:::note
引数作成時には、データ型の変換や初期化なども考慮が必要です。
これらの操作に過剰な時間がかかる場合、全体の処理時間を押し上げることになるため、計算量の最適化が重要です。
:::

#### 引数サイズ(Sarg)
呼び出し時に渡すデータのサイズです。
データ量が多ければ多いほど、転送に時間がかかり、処理全体の遅延につながります。
そのため、必要最小限のデータを渡すことが重要です。

:::note
引数サイズを減らすための工夫として、プロトコルの選定（例えば、軽量な形式のデータを使う）や、
圧縮技術の利用が挙げられます。
:::

#### 処理時間(Tm)
呼び出し先の処理が実際に行われる時間です。
この時間を短縮するためには、アルゴリズムの最適化やデータ構造の改善が有効です。
また、非同期処理を導入することで、待機時間を他の処理に使うといった工夫も考えられます。

:::note
処理の時間短縮の際には、キャッシュ戦略を取り入れることも一つの手段です。
計算済みの結果を再利用することで、処理時間を大幅に削減できます。
:::

#### 結果サイズ(Sres)
呼び出し先から返されるデータのサイズです。
結果が大きいと、処理の後続ステップに負荷をかける可能性があります。
そのため、必要な情報だけを抽出するか、データの圧縮を検討することが重要です。
また、結果となるデータの構造を正規化するなどの工夫を施すことで、解析にかかる時間を短縮することもできます。

:::note
必要な情報だけを抽出するフィルタリングを行うことで、後続の処理を軽減できます。
データを小さく保つことは、システム全体の効率化につながります。
:::

#### 結果解析処理(Pres)
呼び出しから得られたデータを目的に応じて処理・整形するステップです。
この処理が複雑であればあるほど、全体の処理時間に影響します。
そのため、解析のロジックをシンプルに保つこと、または解析処理を複数のプロセスに分けて並列化することが効果的です。

:::note
結果解析の部分では、必要な部分のみを抜き出して効率的に処理することが鍵となります。
データの正規化や変換が不要な場合、それを省くことで高速化が図れるケースも存在します。
:::

### まだ終わらない

普段みなさんが触れているプログラムは、これだけでは終わらないと思います。
アルゴリズムには繰り返し処理が含まれる為、呼び出し先のProcessBは何度も呼び出されます。

繰り返しも追加した場合、このような図になります。
```mermaid
sequenceDiagram
    participant T as 処理時間(T)
    participant A as Process A
    participant B as Process B
    
    T->>+A: ping!
    alt ↓処理回数(N)
        A-->>A: 引数作成処理(Parg)
        A->>+B: call(argN)
        Note over A,B: 引数サイズ(Sarg)→
        Note right of B: ↓処理時間(Tm)
        B->>-A: resN
        Note over A,B: ←結果サイズ(Sres)
        A-->>A: 結果解析処理(Pres)
    else
        A->>+B: ...call(argNF)
        B->>-A: ...resNF
    end
    A->>-T: pong!
```

#### 処理回数(N)
呼び出し先の処理が繰り返される回数です。
繰り返し回数が増えると全体の処理時間も増加するため、可能な限り繰り返しを減らす工夫が求められます。
ネットワークやI/Oのオーバーヘッドを減らし、効率化を図ることが重要です。


## 高速化のアプローチ
ここでは、ServerとDBを使って解消のアプローチの具体例を説明します。
BrowserとBFFに置き換えて理解してもらっても構いません。

### 一括処理
一括処理は、複数の処理をまとめて行うことで、処理のオーバーヘッドを減らし、全体的な速度を向上させる手法です。
例えば、データベースへのクエリ発行を個別に行うのではなく、一度に複数のクエリをまとめて実行することで、通信の回数を減らし、応答時間を短縮することができます。

```mermaid
sequenceDiagram
    participant T as 処理時間(T)
    participant A as Server
    participant B as DB
    
    T->>+A: Request!
    alt ↓処理回数(▲N)
        A-->>A: 引数作成処理(△Parg)
        A->>+B: call(△...argN)
        Note over A,B: 引数サイズ(△Sarg)→
        Note right of B: ↓処理時間(△Tm)
        B->>-A: resN
        Note over A,B: ←結果サイズ(△Sres)
        A-->>A: 結果解析処理(△Pres)
    end
    A->>-T: Response!
```

:::note
一括処理を行う場合、データベースやサーバーの負荷に注意する必要があります。
過剰に大きなバッチは逆にシステムに負担をかけることがあるため、適切なサイズに調整することが重要です。
:::

### 並列化
並列化は、複数の処理を同時に実行することで、処理速度を向上させる手法です。
特に、独立したタスクが複数ある場合、それぞれを別々のスレッドやプロセスで実行することで、CPUの使用率を最大化し、待機時間を減少させることが可能です。

例えば、Webサーバーが複数のリクエストを同時に処理する場合、並列化を行うことで全体の応答性が向上します。

```mermaid
sequenceDiagram
    participant T as 処理時間(T)
    participant A as Server
    participant B as DB
    
    T->>+A: Request!
    par ↓処理回数(N)
        A-->>A: 引数作成処理(Parg)
        A->>+B: callA(argN)
        A->>+B: callB(argN)
        Note over A,B: 引数サイズ(Sarg)→
        Note right of B: ↓処理時間(Tm)<br>△△△負荷
        B->>-A: resN B
        B->>-A: resN A
        Note over A,B: ←結果サイズ(Sres)
        A-->>A: 結果解析処理(Pres)
    end
    A->>-T: Response!
```

:::note
並列化を行う際には、競合状態（レースコンディション）に注意が必要です。
適切にロックを管理し、データの整合性を保つことが、正しい結果を得るために不可欠です。
:::

## さいごに

このパフォーマンスに対するメンタルモデルは、電気信号が伝導体を流れる限り必ず発生します。
処理時間(T)はシステム構成によってそれぞれの変数の最適値が異なります。
高速化を行う際は、それぞれの変数のどれがボトルネックになっているのか粘り強く最適化を行う事も必要となります。

例えば、
「ローカル環境では高速だったが本番環境では遅い」
　→「結果サイズ(Sres)のネットワーク帯域がボトルネックになっている！」
「リリース当初は問題なかったが、データ量増加に伴い急激に遅くなる」
　→ 「データベースの実行計画=処理時間(tM)に変更が発生した！」
など、様々なケースがあり最適化する必要性に駆られます。

基本的に高速である事はリソースの消費を抑える事ができ、システムの安定化にも繋がります。
ですが最適化には「早すぎる最適化問題」などもあり、
どこまで高速化すれば適切な(保守も含めた)工数等とトレードオフでビジネスインパクトを得られるかを判断する事も重要となります。

高速化する事が楽しくなっていくらでも最適化を続けてしまいたくなる気持ちもわかりますが、程々に。
ぐっどらっく！