ソフトウェアを最適化したいとき、最初に取るステップの1つはプロファイリングデータを確認することです。ソフトウェアプロファイラは、ソフトウェアがどこで時間を費やしているかを特定しようとするツールです。手法は様々ですが、典型的なプロファイラはソフトウェアをサンプリング(定期的に実行を停止)して統計を収集します。ある関数でソフトウェアが頻繁に停止する場合、その関数が多くの時間を消費している可能性が高く、最適化の対象となることがあります。

Matteo Collinaは最近、JavaScriptでのソフトウェア最適化のためにプロファイラーデータをフィードする自身の取り組みを共有してくれました。本質的に、Matteoはプロファイリングデータを取得し、AIが理解できる形に整えます。洞察はシンプルですが興味深いものです。AIにプロファイリングデータの取得方法を伝え、その後、コードを繰り返しプロファイリングしながら最適化させるというものです。このアイデアはAIツール自体がプロファイリングデータを取得できることに気づくため、目新しいものではありません。

どれくらい効果があるのでしょうか。実際に試してみました。

ケース1. コード統合スクリプト

simdutfソフトウェアライブラリでは、統合スクリプトを使用しています。このスクリプトはディスク上のすべてのC++ファイルを収集し、簡易的なパースを行い、一定のルールに従ってそれらを結合します。

まず、プロファイリングデータなしでAIにスクリプトの最適化を依頼しました。AIはすぐにファイルキャッシュを追加しました。このスクリプトは同じファイルを繰り返しディスクから読み込むため(スクリプトは少し複雑です)、これにより実行時間の約20%を削減できました。

具体的には、AIは以下のような素朴なコードを…

def read_file(file):
    with open(file, 'r') as f:
        for line in f:
            yield line.rstrip()

キャッシュ付きの以下のバージョンに置き換えました…

def read_file(file):
    if file in file_cache:
        for line in file_cache[file]:
            yield line
    else:
        lines = []
        with open(file, 'r') as f:
            for line in f:
                line = line.rstrip()
                lines.append(line)
                yield line
        file_cache[file] = lines

プロファイリングデータがあればAIはさらに改善できるでしょうか。Pythonプロファイラを実行するよう指示しました:python -m cProfile -s cumtime myprogram.py。AIは2つの追加最適化を発見しました。

1. 正規表現を事前コンパイルしました(re.compile)。以下を

  if re.match('.*generic/.*.h', file):
    # ...

以下のように置き換えました。

if generic_pattern.match(file):
    # ...

コードの別の場所では以下のように定義しています…

generic_pattern = re.compile(r'.*generic/.*\.h')

2. 文字列の正規表現置換でre.subを繰り返し呼び出す代わりに、まずキーワードの存在をチェックして文字列をフィルタリングするようにしました。

if 'SIMDUTF_IMPLEMENTATION' in line: # This IF is the optimization
  print(uses_simdutf_implementation.sub(context.current_implementation+"\\1", line), file=fid)
else:
  print(line, file=fid) # Fast path

これら2つの最適化はおそらくコードを直接見れば到達できたもので、プロファイリングデータに基づいたものかどうかは確信できません。しかし、プロファイルデータに実際に現れていることはわかります。

残念ながら、ファイルアクセスのキャッシュという低コストで得られる成果が改善の大部分を占めました。AIはコードをさらに最適化できませんでした。そのため、プロファイリングデータはあまり役に立ちませんでした。

ケース2: リンク確認スクリプト

オンラインコースを設計する際、リンクを多用することがあります。これらのリンクは時間の経過とともに壊れます。そこで、すべてのリンクを走査して検証するシンプルなPythonスクリプトを作成しました。

まず、AIにコードの最適化を依頼しました。AIは同じ正規表現のコンパイルを行い、スレッドプールを作成してスクリプトを非同期化しました。

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    url_results = {url: executor.submit(check_url, url) for url in urls_to_check}
    for url, future in url_results.items():
        url_cache[url] = future.result()

この並列化により、スクリプトの速度は2倍以上になりました。

AIはfunctoolsを使用してURLチェックを興味深い方法でキャッシュしました。

from functools import lru_cache

@lru_cache(maxsize=None)
def check(link):
    # ...

私はこの便利な手法を知りませんでした。ただし、同じリンクが複数回登場することは稀であるため、このコンテキストでは役に立ちませんでした。

次に、プロファイラを使用するよう再度指示しました。AIは正規表現の最適化を除いてほぼ同じことを行いました。

私の知る限り、マルチスレッド化以外の最適化はすべて無駄でした。また、この部分はプロファイリングデータなしでも実行できました。

現時点での結論

私が試したPythonスクリプトは、パフォーマンスが重要ではなかったため、十分に最適化されていませんでした。これらは比較的シンプルです。

統合スクリプトでは、ファイルキャッシュのおかげで「無料」で20%のパフォーマンス向上を得られました。リンクチェッカーはマルチスレッド化により高速化されます。どちらの最適化も有効で有用であり、私の生活を少しだけ改善してくれます。

どちらのケースでも、プロファイラーデータによる利点は確認できませんでした。私は当初、AIにコードをループで最適化させ、継続的にプロファイラを実行させることを期待していましたが、これらの単純なケースでは実現しませんでした。AIはプロファイラーデータによると実行時間への寄与が少ないコードセグメントを最適化しました。

公平に言えば、プロファイリングデータの有用性は限定的であることが多いです。実際の問題はアーキテクチャ的なものであり、狭いボトルネックとは関係がないことがよくあります。識別可能なボトルネックが存在する場合でも、単純なプロファイリング実行ではそれらを明確に特定できないことがあります。さらに、プロファイラはコードベースが大きくなるにつれて有用になりますが、私のテストケースは非常に小さいものです。

全体として、私の相対的な失敗の主な理由は、適切なユースケースがなかったことだと考えています。プロファイリングデータを収集し、AIに確認を依頼することは、現時点では合理的な最初のステップになると思います。