web-dev-qa-db-ja.com

低レイテンシコードは時々 "醜い"必要があるのでしょうか?

(これは主に、根拠のない意見で答えるだけの人々を避けるために、低遅延システムの特定の知識を持つ人を対象としています)。

「Nice」オブジェクト指向のコードを書くことと、非常に高速で低レイテンシのコードを書くことの間にトレードオフがあると感じますか?たとえば、C++での仮想関数の回避/ポリモーフィズムのオーバーヘッドなど-見栄えが悪いが、非常に高速なコードを書き直すなど。

それが理にかなっている-それが醜く見えるかどうか(誰が維持できる限り)を気にするか-あなたがスピードを必要とするなら、あなたはスピードが必要ですか?

そういう分野で働いてきた人から聞いてみたいです。

21
user997112

「Nice」オブジェクト指向のコードを書くことと非常に[sic]の低レイテンシコードを書くことの間にトレードオフがあると感じますか?

はい。

そのため、「時期尚早の最適化」というフレーズが存在します。開発者にmeasureのパフォーマンスを強制し、パフォーマンスに違いをもたらすになるコードのみを最適化しながら、アプリケーションアーキテクチャを最初から慎重に設計して、重い負荷がかかっても落下しない。

そうすることで、可能な限り最大限に、きちんと設計されたオブジェクト指向のコードを維持し、重要な小さな部分だけを醜いコードで最適化することができます。

31
Robert Harvey

はい、私が与える例はC++対Javaではありませんが、アセンブリ対COBOLです)。

どちらの言語も非常に高速ですが、コンパイル時のCOBOLでさえ、必ずしもそこにある必要はない命令セットに配置された多くの命令があります。

C++で継承/ポリモーフィズムを使用するのではなく、「見苦しいコード」を作成するという質問に同じ考え方を直接適用できます。見苦しいコードを書く必要があると思います。エンドユーザーが1秒未満のトランザクションタイムフレームを必要とする場合、どのようにしてそれを提供するかはプログラマーとしての私たちの仕事です。

とは言っても、コメントを自由に使用すると、コードがどれほど醜いかに関係なく、プログラマーの機能性と保守性が大幅に向上します。

5
Josh Tollefson

はい、トレードオフが存在します。これにより、より高速で醜いコードは必ずしも適切ではないことを意味します。「高速コード」の定量的なメリットは、メンテナンスの複雑さに対して重み付けする必要があります。その速度を達成するために必要なコードの変更。

トレードオフはビジネスコストから発生します。より複雑なコードは、より熟練したプログラマー(およびCPUアーキテクチャと設計知識を備えたものなど、より集中したスキルセットを持つプログラマー)を必要とし、コードを読んで理解し、バグを修正します。このようなコードの開発と保守にかかるビジネスコストは、通常作成されたコードの10倍から100倍の範囲になる可能性があります。

この保守コストは一部の業界では正当化できます、顧客は非常に高速なソフトウェアに対して非常に高いプレミアムを支払う用意があります。

一部の速度最適化は、他よりも投資収益率(ROI)が向上します。つまり、一部の最適化手法は、コードの保守性(より高いレベルの構造とより低いレベルの読みやすさの維持)への影響が少ない状態で適用できます。通常書かれたコードに。

したがって、事業主は次のことを行う必要があります:

  • コストとメリットを見て、
  • 測定と計算を行う
    • プログラマにプログラム速度を測定してもらいます
    • プログラマーに最適化に必要な開発時間を見積もらせる
    • より高速なソフトウェアによる収益増加について独自の見積もりを作成
    • ソフトウェアアーキテクトまたはQAマネージャーに、ソースコードの直感性と可読性の低下による欠点を定性的に評価させる
  • そして、ソフトウェアの最適化の成果を優先します。

これらのトレードオフは状況に非常に固有です。

これらは、マネージャーと製品所有者の参加なしに最適に決定することはできません。

これらはプラットフォームに非常に固有です。たとえば、デスクトップCPUとモバイルCPUでは考慮事項が異なります。サーバーアプリケーションとクライアントアプリケーションの考慮事項も異なります。


はい、通常、より高速なコードは通常作成されたコードとは異なるように見えます。異なるコードはすべて、読み取るのに時間がかかります。それが醜さを暗示するかどうかは見る人の目にあります。

(ある程度の専門知識を要求することなく)短いベクトルの最適化(SIMD)、きめ細かいタスクの並列処理、メモリの事前割り当て、およびオブジェクトの再利用。

SIMDは通常、高レベルの構造変更を必要としない場合でも(APIがボトルネック防止を考慮して設計されている場合)、低レベルの可読性に深刻な影響を与えます。

一部のアルゴリズムは、SIMDに簡単に変換できます(恥ずかしいほどベクトル化可能)。一部のアルゴリズムでは、SIMDを使用するためにより多くの計算の再配置が必要です。ウェーブフロントSIMD並列処理などの極端な場合、完全に新しいアルゴリズム(および特許取得可能な実装)を利用するには、そのアルゴリズムを記述する必要があります。

きめの細かいタスクの並列化では、アルゴリズムをデータフローグラフに再配置し、マージン(利益)がそれ以上得られなくなるまで、アルゴリズムに機能(計算)分解を繰り返し適用する必要があります。分解されたステージは通常、関数型プログラミングから借用した概念である継続スタイルと連鎖します。

機能的な(計算)分解により、通常は線形で概念的に明確なシーケンス(書き込まれた順序で実行可能なコード行)で記述できるアルゴリズムをフラグメントに分解し、複数の関数に分散する必要があります。またはクラス。 (以下のアルゴリズムのオブジェクト化を参照してください。)この変更は、そのようなコードを生み出した分解設計プロセスに精通していない他のプログラマーを大幅に妨害します。

このようなコードを保守可能にするには、そのようなコードの作成者は、アルゴリズムの精巧なドキュメントを作成する必要があります。これは、通常記述されるコードに対して行われるコードコメントやUMLダイアグラムの種類をはるかに超えています。これは、研究者が学術論文を書く方法に似ています。


いいえ、高速コードはオブジェクト指向と矛盾する必要はありません。

別の言い方をすれば、まだオブジェクト指向である非常に高速なソフトウェアを実装することが可能です。ただし、その実装の下端に向かって(計算の大部分が発生する根本的なレベルで)、オブジェクト設計はオブジェクト指向設計(OOD)から得られた設計から大幅に逸脱する場合があります。下位レベルの設計は、アルゴリズムのオブジェクト化を対象としています。

カプセル化、ポリモーフィズム、構成などのオブジェクト指向プログラミング(OOP)のいくつかの利点は、低から得ることができます- level algorithm-objectification。これは、OOPをこのレベルで使用することの主な理由です。

オブジェクト指向設計(OOD)のほとんどの利点が失われます。最も重要なのは、低レベルの設計には直感性がないことです。仲間のプログラマーは、最初にアルゴリズムがどのように変換および分解されたかを完全に理解しないと、下位レベルのコードの操作方法を学ぶことができず、この理解は結果のコードからは得られません。

3
rwong

私が見たいくつかの研究は、複雑で読みにくいコードよりも、きれいで読みやすいコードの方が速いことが多いことを示しています。一部には、これはオプティマイザの設計方法によるものです。それらは、計算の中間結果で同じことを行うよりも、変数をレジスターに最適化する方がはるかに優れている傾向があります。最終結果につながる単一の演算子を使用した割り当ての長いシーケンスは、長く複雑な方程式よりも最適化される場合があります。新しいオプティマイザは、クリーンなコードと複雑なコードの違いを減らしたかもしれませんが、それらがそれを排除したとは思えません。

必要に応じて、ループの展開などの他の最適化をクリーンな方法で追加できます。

パフォーマンスを改善するために追加された最適化には、適切なコメントを添付する必要があります。これには、最適化として、できれば前後のパフォーマンスの測定値とともに追加されたという記述を含める必要があります。

80/20ルールが、最適化したコードに適用されることがわかりました。経験則として、80%以上の時間を費やしていないものは最適化しません。次に、10倍のパフォーマンス向上を目指します(通常は達成します)。これにより、パフォーマンスが約4倍向上します。私が実装したほとんどの最適化では、コードの「美しさ」が大幅に低下していません。あなたのマイレージは異なる場合があります。

2
BillThor

醜いとは、他の開発者がそれを再利用したり、理解したりする必要があるレベルで読み取り/理解が難しいことを意味する場合、エレガントで読みやすいコードは、ほとんどの場合最終的に維持する必要のあるアプリの長期的なパフォーマンスの向上。

それ以外の場合は、キラーインターフェースを備えた美しいボックスに醜いものを入れる価値があるほどパフォーマンスに勝てる場合がありますが、私の経験では、これはかなりまれなジレンマです。

あなたが行くように基本的な仕事の回避について考えてください。パフォーマンスの問題が実際に発生するときのために、難解なトリックを保存します。そして、誰かが特定の最適化に精通して初めて理解できる何かを書く必要がある場合は、少なくともコードの観点から再利用して醜い人を理解しやすくするためにできることをしてください。開発者が次の人が何を継承するかについて過度に考えているため、惨めに実行することはめったにありませんが、頻繁な変更がアプリの唯一の定数である場合(私の経験ではほとんどのWebアプリ)、変更するのが難しいのは、パニックになった混乱がコードベース全体にポップアップし始めるのを実際に懇願することです。クリーンでリーンは、長期的にはパフォーマンスに優れています。

2
Erik Reppen

はい、必要な時間で機能させるためにコードを「醜い」ものにする必要がある場合がありますが、すべてのコードが醜い必要はありません。 「醜い」必要があるコードのビットを見つける前に、パフォーマンスをテストしてプロファイルを作成し、それらのセクションにコメントを付けて、将来の開発者が意図的に醜いものと怠惰なものを知るようにする必要があります。誰かがパフォーマンス上の理由を主張して、うまく設計されていないコードをたくさん書いているなら、それを証明させてください。

速度は、プログラムの他の要件と同様に重要です。誘導ミサイルに誤った修正を加えることは、衝突後に正しい修正を提供することと同じです。保守性は、作業中のコードにとって常に二次的な問題です。

2
Ryathal

Complexuglyは同じものではありません。多くの特別なケースがあり、パフォーマンスの最後のすべての低下を精査するように最適化され、最初は 接続と依存関係のもつれ のように見えるコードは、実際には非常に注意深く設計されており、理解できれば非常に美しいかもしれませんそれ。実際、非常に複雑なコードを正当化するためにパフォーマンス(レイテンシなどで測定されたかどうか)が十分に重要である場合、コードが適切に設計されている必要があります。そうでない場合、そのすべての複雑さが単純なソリューションよりも本当に優れていると確信することはできません。

私にとって醜いコードは、ずさんなコードであり、検討が不十分であり、不必要に複雑です。実行する必要のあるコードにこれらの機能が必要だとは思わない。

1
Caleb

「Nice」オブジェクト指向のコードを書くことと、非常に高速で低レイテンシのコードを書くことの間にトレードオフがあると感じますか?たとえば、C++での仮想関数の回避/ポリモーフィズムのオーバーヘッドなど-見栄えが悪いが非常に高速なコードの再作成など。

私はレイテンシよりもスループットを重視する分野で働いていますが、それはパフォーマンスが非常に重要であり、 "sorta"と言います。 。

しかし、問題は、非常に多くの人々がパフォーマンスの概念を完全に間違っているということです。初心者は多くの場合、すべてのことを間違えるだけであり、「計算コスト」の概念モデル全体を再構築する必要があります。アルゴリズムの複雑さだけが、正しいことができる唯一のものです。中間者は多くのことを間違っています。専門家はいくつかのことを間違っています。

キャッシュミスやブランチの予測ミスなどの指標を提供できる正確なツールを使用して測定することで、あらゆるレベルの専門知識を持つすべての人々を抑制できます。

測定は、最適化すべきでないことを指摘することでもあります。エキスパートは、真の測定されたホットスポットを最適化し、何が遅くなる可能性があるかについての直感に基づいて暗闇の中でワイルドスタブを最適化しないため、初心者よりも最適化に少ない時間を費やすことがよくあります(これは、極端な形では、コードベースの他のほぼすべての行をマイクロ最適化したくなるかもしれません).

パフォーマンスの設計

それはさておき、パフォーマンスの設計の鍵は、インターフェース設計のように、designの部分にあります。経験不足の問題の1つは、一部の一般化されたコンテキストでの間接関数呼び出しのコストのように、コスト(オプティマイザの観点からすぐに理解する方がよく理解できる)のように、絶対的な実装メトリックが早期にシフトする傾向があることです。分岐の視点ではなく視点)は、コードベース全体でそれを回避する理由です。

コストは相対的です間接的な関数呼び出しにはコストがありますが、たとえば、すべてのコストは相対的です。何百万もの要素をループする関数を呼び出すためにそのコストを1回支払う場合、このコストを心配することは、数十億ドルの製品の購入にペニーを何時間も費やすことと同じです。 1ペニーが高すぎました。

より粗いインターフェースの設計

パフォーマンスのインターフェースdesignの側面は、多くの場合、これらのコストをより粗いレベルにプッシュするために、以前に求められています。たとえば、単一のパーティクルに対して実行時の抽象化コストを支払う代わりに、そのコストをパーティクルシステム/エミッターのレベルにプッシュして、パーティクルを実装の詳細やこのパーティクルコレクションの単純な生データに効果的にレンダリングできます。

したがって、オブジェクト指向の設計は、パフォーマンスの設計(レイテンシでもスループットでも)と互換性がある必要はありませんが、言語にますます細かいオブジェクトをモデル化することに焦点を当てた誘惑があり、最新のオプティマイザーではそれができません助けて。ソフトウェアのメモリアクセスパターンの効率的なSoA表現を生成する方法で、単一のポイントを表すクラスを合体するようなことはできません。粗さのレベルでモデル化されたインターフェース設計のポイントのコレクションは、その機会を提供し、必要に応じてより多くの最適なソリューションに向かって反復することを可能にします。このような設計は、大容量メモリ*向けに設計されています。

*ここではmemoryに重点を置いており、dataではなく、パフォーマンスが重要な領域で長時間作業していることに注意してください。は、データタイプとデータ構造のビューを変更し、それらがメモリに接続する方法を確認する傾向があります。バイナリ検索ツリーは、ツリーノードの潜在的に分散し、キャッシュに適さないメモリチャンクなどの場合に、対数の複雑さだけで問題になることはありません。固定アロケータ。ビューはアルゴリズムの複雑さを解消しませんが、メモリレイアウトとは無関係に見えなくなります。また、作業の反復がメモリアクセスの反復についてであると見なし始めます。*

多くのパフォーマンス重視の設計は、実際には、人間が理解して使用しやすい高レベルのインターフェース設計の概念と非常に互換性があります。違いは、このコンテキストでの「高レベル」は、メモリのバルク集約、潜在的に大規模なデータのコレクション用にモデル化されたインターフェース、およびその内部での実装に関するものであることです。かなり低レベルかもしれません。視覚的な例えは、非常に快適で運転や取り扱いが簡単で、音速で移動しているときに非常に安全な車ですが、ボンネットを開けると、内部に火を噴く悪魔はほとんどいません。

より粗い設計では、より効率的なロックパターンを提供し、コードの並列処理を利用するより簡単な方法になる傾向もあります(マルチスレッドは、ここではスキップしますが、網羅的な主題です)。

メモリプール

低レイテンシのプログラミングの重要な側面は、メモリの割り当てと割り当て解除の一般的な速度だけでなく、参照の局所性を改善するためのメモリの非常に明示的な制御になるでしょう。メモリをプールするカスタムアロケーターは、実際には、私たちが説明したのと同じ種類の設計思想を反映しています。 bulk;のために設計されています。大まかなレベルで設計されています。大きなブロックにメモリを事前に割り当て、小さなチャンクにすでに割り当てられているメモリをプールします。

アイデアは、コストのかかるもの(たとえば、汎用アロケーターに対してメモリチャンクを割り当てる)をより粗いレベルにプッシュすることとまったく同じです。メモリプールは、メモリを一括して処理するために設計されています

型システム分離メモリ

任意の言語での詳細なオブジェクト指向設計の難しさの1つは、多くの小さなユーザー定義型とデータ構造を導入したい場合があることです。これらの型は、動的に割り当てられる場合、小さな小さな塊に割り当てられるようにすることができます。

C++の一般的な例は、ポリモーフィズムが必要な場合です。自然な誘惑は、汎用メモリアロケータに対してサブクラスの各インスタンスを割り当てることです。

これにより、連続する可能性のあるメモリレイアウトが、アドレス範囲全体に散らばった小さなビットとビットに分割され、ページフォールトとキャッシュミスが増加します。

レイテンシが最も低く、途切れのない、確定的な応答を要求するフィールドは、ホットスポットが常に単一のボトルネックにまで沸騰するとは限らない1つの場所であり、小さな非効率性が実際に「蓄積」することができる(多くの人がプロファイラーを使用して、それらをチェックするために誤って発生すると想像していますが、レイテンシ主導のフィールドでは、ごくわずかな非効率が蓄積するまれなケースが実際にあります)。そして、そのような蓄積の最も一般的な理由の多くはこれである可能性があります:至る所に小さなメモリの小さなチャンクが過剰に割り当てられています。

Javaのような言語では、intの配列などのボトルネック領域(タイトループで処理される領域)で可能な場合は、プレーンな古いデータ型の配列をさらに使用すると便利です(ただし、かさばる高レベルの背後にあります)。インターフェース)たとえば、ユーザー定義のArrayListオブジェクトのIntegerの代わりに。これにより、通常後者に伴うメモリ分離が回避されます。 C++では、メモリ割り当てパターンが効率的であれば、ユーザー定義型をそこに連続して割り当てたり、ジェネリックコンテナーのコンテキストで割り当てたりすることができるので、構造をそれほど劣化させる必要はありません。

メモリの融合

ここでのソリューションは、同種のデータ型のカスタムアロケーターに到達することです。小さなデータ型とデータ構造がメモリ内のビットとバイトにフラット化されると、それらは均一な性質を帯びます(ただし、いくつかの異なる配置要件があります)。それらをメモリ中心の考え方から見ない場合、プログラミング言語の型システムは、潜在的に隣接するメモリ領域を小さな小さな分散したチャンクに分割/分離することを「望んでいます」。

スタックは、このメモリ中心のフォーカスを利用してこれを回避し、ユーザー定義型インスタンスの可能な混合された組み合わせをその中に潜在的に格納します。スタックをより多く利用することは、可能な場合、その最上部がほとんどキャッシュラインにあるので素晴らしいアイデアですが、LIFOパターンなしでこれらの特性の一部を模倣するメモリアロケータを設計することもできます。 、より複雑なメモリの割り当てと割り当て解除のパターンでも、異種のデータ型間でメモリを連続したチャンクに融合します。

最新のハードウェアは、メモリの連続したブロックを処理するとき(たとえば、同じキャッシュライン、同じページに繰り返しアクセスする)がピークになるように設計されています。キーワードには隣接性があります。これは、関心のある周囲のデータがある場合にのみ有益であるためです。したがって、パフォーマンスの鍵となる多くの(そして難しさもある)のは、分離されたメモリチャンクを、連続して(連続するすべてのデータが関連する)アクセスされるブロックに融合し、追い出すことです。プログラミング言語の特にユーザー定義型の豊富な型システムは、ここで最大の障害になる可能性がありますが、カスタムアロケーターや必要に応じてかさばる設計を介して問題にいつでも到達して解決できます。

醜い

「醜い」とは言い難い。これは主観的な測定基準であり、パフォーマンスが非常に重要な分野で働く人は、「美」の考え方を、よりデータ指向で物事を一括で処理するインターフェースに焦点を当てたものに変え始めます。

危険な

「危険」の方が簡単かもしれません。一般に、パフォーマンスは低レベルのコードに到達したいと思う傾向があります。たとえば、メモリアロケータの実装は、データ型の下に到達し、生のビットとバイトの危険なレベルで作業しないと不可能です。その結果、これらのパフォーマンスが重要なサブシステムでの注意深いテスト手順に重点を置き、最適化のレベルを適用してテストの完全性を拡大することができます。

美しさ

しかし、これはすべて実装詳細レベルになります。ベテランの大規模でパフォーマンス重視の考え方では、「美しさ」は実装の詳細ではなく、インターフェースの設計にシフトする傾向があります。インターフェース設計の変更に直面して発生する可能性のある結合とカスケードの破損により、実装ではなく「美しい」、使用可能で安全な効率的なインターフェースを探すことが、指数関数的に優先度が高くなります。実装はいつでも交換できます。通常、必要に応じて、また測定結果から指摘されるように、パフォーマンスに向かって反復します。インターフェイス設計の鍵は、システム全体を壊すことなく、そのような反復のための余地を残すのに十分粗いレベルでモデル化することです。

実際、ベテランがパフォーマンス重視の開発に重点を置くと、多くの場合、安全性、テスト、保守性、主にSEの弟子に重点が置かれる傾向があることを示唆します。重要なサブシステム(パーティクルシステム、画像処理アルゴリズム、ビデオ処理、オーディオフィードバック、レイトレーサー、メッシュエンジンなど)は、メンテナンスの悪夢に溺れないように、ソフトウェアエンジニアリングに細心の注意を払う必要があります。多くの場合、驚くほど効率の高い製品でもバグの数が最も少ないのは、単なる偶然ではありません。

TL; DR

とにかく、それは、本当にパフォーマンスが重要なフィールドでの優先順位、レイテンシを削減してわずかな非効率性を蓄積させる可能性のあるもの、および実際に「美しさ」を構成するもの(最も生産的に見た場合)に及ぶ、私のテーマです。

1
user204677

違いはありませんが、これが私がすることです:

  1. クリーンでメンテナンスしやすいように書いてください。

  2. パフォーマンス診断 を実行し、推測された問題ではなく、それが示す問題を修正します。保証された、彼らはあなたが期待するものとは異なります。

これらの修正は、まだ明確で保守可能な方法で実行できますが、コメントを追加して、コードを見た人がなぜそのようにしたのかわかるようにする必要があります。そうしないと、元に戻されます。

それでトレードオフはありますか?私はそうは思いません。

0
Mike Dunlavey

非常に高速な醜いコードを書くことができ、醜いコードと同じくらい速い美しいコードを書くこともできます。ボトルネックは、コードの美しさ/組織/構造ではなく、選択した手法にあります。たとえば、非ブロッキングソケットを使用していますか?シングルスレッド設計を使用していますか?スレッド間通信にロックフリーキューを使用していますか? GCのゴミを出していますか?重要なスレッドでブロッキングI/O操作を実行していますか?ご覧のとおり、これは美しさとは関係ありません。

0
rdalmeida

エンドユーザーは何を問題にしますか?

  • パフォーマンス
  • 特徴/機能
  • 設計

ケース1:最適化された不良コード

  • ハードメンテナンス
  • オープンソースプロジェクトの場合、ほとんど読めない

ケース2:最適化されていない適切なコード

  • メンテナンスが簡単
  • 悪いユーザー体験

解決?

簡単で、パフォーマンスが重要なコードを最適化

例えば。:

5メソッドで構成されるプログラム。そのうち3つはデータ管理用、1つはディスク読み取り用、もう1つはディスク書き込み用です。

これら3つのデータ管理方法は2つのI/O方法を使用し、それらに依存しています

I/Oメソッドを最適化します。

理由: I/Oメソッドが変更される可能性が低く、アプリのデザインに影響もありません。全体として、そのプログラムのすべてがメソッドに依存しているため、パフォーマンスが重要と思われます。それらを最適化するためにどんなコードを使用するでしょう。

これは、コードの特定の部分を最適化することでプログラムを高速に保ちながら、優れたコードとプログラムの管理しやすい設計を実現することを意味します

私は考えています..

悪いコードは人間が磨き最適化するのを難しくし、小さな間違いはそれをさらに悪化させるかもしれないと思うので、その醜いコードをうまく書けば初心者/初心者向けの良いコードがより良いでしょう。

0
OverCoder