早すぎる最適化はすべての悪の根源であることは誰もが知っています。さらに悪いのは悲観化です。誰かが「最適化」を実装すると、それはより高速になると考えられますが、遅くなり、バグが多く、メンテナンスが困難になります。 ?
早すぎる最適化はすべての悪の根源であることは誰もが知っています。さらに悪いのは悲観化です。誰かが「最適化」を実装すると、それはより高速になると考えられますが、遅くなり、バグが多く、メンテナンスが困難になります。 ?
回答:
古いプロジェクトでは、Z-8000の経験が豊富な(そうでない場合は優れた)組み込みシステムプログラマーを何人か引き継ぎました。
新しい環境は32ビットのSparc Solarisでした。
RAMから16ビットを取得する方が32ビットを取得するよりも高速だったので、1人が行って、コードを高速化するためにすべてのintをshortに変更しました。
私は、32ビットシステムで32ビット値を取得する方が16ビット値を取得するよりも高速であることを示し、CPUが16ビット値を取得するには32ビット幅にする必要があることを説明するデモプログラムを作成する必要がありました。メモリアクセスし、16ビット値に不要なビットをマスクまたはシフトします。
私は、「時期尚早な最適化はすべての悪の根源である」という言い回しは、ずっと使われている方法だと思います。多くのプロジェクトでは、プロジェクトの後半までパフォーマンスを考慮しないことが言い訳になっています。
多くの場合、このフレーズは仕事を避けるための松葉杖です。この言葉は、「ああ、私たちは前もってそのことを本当に考えていなかったし、今はそれに対処する時間がない」と言うときに使われるのを見ています。
「悲観化」が原因で発生する問題の例よりも、ダムのパフォーマンス問題の「とんでもない」例の方がはるかに多い
私がより良い発言だと思うのは、「測定も理解もしない最適化は、最適化ではありません-ただのランダムな変化」です。
優れたパフォーマンス作業には時間がかかります。多くの場合、機能またはコンポーネント自体の開発に時間がかかります。
データベースは悲観化の遊び場です。
お気に入りは次のとおりです。
それは私の頭の上からです。
絶対的なルールはないと思います。最初に最適化されるものとそうでないものがあります。
たとえば、衛星からデータパケットを受信する会社で働いていました。各パケットは多額の費用がかかるため、すべてのデータは高度に最適化されました(つまり、パックされました)。たとえば、緯度/経度は絶対値(浮動小数点数)ではなく、「現在の」ゾーンの「北西」のコーナーに対するオフセットとして送信されました。使用する前にすべてのデータを解凍する必要がありました。しかし、これは悲観化ではなく、通信コストを削減するためのインテリジェントな最適化だと思います。
一方、ソフトウェアアーキテクトは、展開されたデータを非常に読みやすいXMLドキュメントにフォーマットし、データベースに保存することを決定しました(対応する列に各フィールドを保存するのではなく)。彼らの考えは、「XMLが未来」、「ディスク容量が安い」、「プロセッサが安い」ので、何も最適化する必要はありませんでした。その結果、16バイトのパケットが1つの列に格納された2kBのドキュメントに変換され、単純なクエリでさえ、メガバイトのXMLドキュメントをメモリにロードする必要がありました。毎秒50パケット以上を受信したので、パフォーマンスがどれほどひどいか想像できます(ところで、会社は倒産しました)。
繰り返しになりますが、絶対的なルールはありません。はい、最適化が早すぎることは間違いです。しかし、「cpu /ディスクスペース/メモリは安い」というモットーがすべての悪の本当のルーツである場合もあります。
良い主よ、私はそれらすべてを見てきました。多くの場合、怠惰すぎてパフォーマンスの問題の原因を突き止めたり、実際にパフォーマンスの問題があるかどうかを調査したりできない、だれかがパフォーマンスの問題を修正するための努力です。これらの多くのケースでは、特定のテクノロジーを試してみて、必死に彼らの光沢のある新しいハンマーに合う釘を探しているその人のケースではないのではないかと思います。
ここに最近の例があります:
データアーキテクトが、かなり大規模で複雑なアプリケーションのキーテーブルを垂直に分割するという手の込んだ提案を思い付きました。彼は、変化に対応するためにどのような開発努力が必要かを知りたいと思っています。会話は次のようになりました:
私:なぜこれを検討しているのですか?解決しようとしている問題は何ですか?
彼:テーブルXは幅が広すぎます。パフォーマンス上の理由から分割しています。
私:それが広すぎると思うのはなぜですか?
彼:コンサルタントは、1つのテーブルに含めるには列が多すぎると言いました。
私:これはパフォーマンスに影響を与えていますか?
彼:はい、ユーザーはアプリケーションのXYZモジュールで断続的な速度低下を報告しています。
私:テーブルの幅が問題の原因であることをどうやって知っていますか?
彼:これはXYZモジュールで使用されるキーテーブルで、200列のようなものです。それは問題だろう。
私(説明):ただし、特にモジュールXYZはそのテーブルのほとんどの列を使用し、ユーザーがそのテーブルから表示するデータを表示するようにアプリを構成しているため、モジュールが使用する列は予測できません。いずれにしても、95%の時間ですべてのテーブルを結合し直すことになり、パフォーマンスが低下する可能性があります。
彼:コンサルタントは幅が広すぎるため、変更する必要があると述べました。
私:このコンサルタントは誰ですか?私たちがコンサルタントを雇ったことも知らなかったし、彼らが開発チームに話しかけたこともまったくなかった。
彼:ええ、まだ採用していません。これは彼らが提案した提案の一部ですが、このデータベースを再構築する必要があると彼らは主張しました。
私:ええと。したがって、データベースの再設計サービスを販売するコンサルタントは、データベースの再設計が必要だと考えています。
会話はこのように続いた。その後、問題のテーブルをもう一度調べて、エキゾチックなパーティショニング戦略を必要とせずに、単純な正規化で狭めることができると判断しました。もちろん、これは(以前は報告されていなかった)パフォーマンスの問題を調査し、それらを2つの要因にまで追跡すると、問題になる可能性があることが判明しました。
もちろん、アーキテクトは依然として、「幅が広すぎる」メタ問題にぶら下がっているテーブルの垂直分割を要求しています。アプリを見たりパフォーマンス分析を実行したりせずにデータベースの大幅な設計変更が必要であると判断できる別のデータベースコンサルタントから提案を得て、彼のケースをさらに強化しました。
私は、alphadrive-7を使用してCHX-LTを完全に培養する人々を見てきました。これは一般的ではありません。より一般的な方法は、ZTトランスフォーマーを初期化してバッファリングを減らし(正味の過負荷抵抗が大きくなるため)、Javaスタイルのバイトグラフィックを作成することです。
全く悲観的!
驚異的なことは何もないと私は認めますが、StringBufferを使用してJavaのループの外で文字列を連結する人々を見つけました。回転のようなシンプルなものでした
String msg = "Count = " + count + " of " + total + ".";
に
StringBuffer sb = new StringBuffer("Count = ");
sb.append(count);
sb.append(" of ");
sb.append(total);
sb.append(".");
String msg = sb.toString();
かなり高速だったため、この手法をループで使用することは非常に一般的でした。実は、StringBufferは同期されているため、いくつかの文字列を連結するだけの場合、実際には余分なオーバーヘッドが生じます。(言うまでもなく、違いはこのスケールでは取るに足らないものです。)このプラクティスに関する他の2つのポイント:
「ルート」テーブルを使用するMSSQLデータベースを見たことがあります。ルートテーブルには、GUID(uniqueidentifier)、ID(int)、LastModDate(datetime)、およびCreateDate(datetime)の4つの列がありました。データベース内のすべてのテーブルは、ルートテーブルに対して外部キーが設定されていました。データベース内の任意のテーブルに新しい行が作成されたときは常に、(データベースがジョブを実行するのではなく)関心のある実際のテーブルに到達する前に、ルートテーブルにエントリを挿入するためにいくつかのストアドプロシージャを使用する必要がありましたいくつかのトリガーを持つ単純なトリガー)。
これは役に立たない傍聴と頭痛の混乱を引き起こし、sprocsを使用するためにその上に何かを書く必要がありました(そして会社にLINQを導入するという私の希望を排除しました。それがすることになっていたことを達成する。
このパスを選択した開発者は、テーブル自体にGUIDを使用しなかったため、これにより大量のスペースが節約されるという前提でそれを守りました(ただし、作成するすべての行のGUIDがルートテーブルで生成されていないのですか?) 、何らかの形でパフォーマンスが向上し、データベースへの変更を「簡単に」監査できるようになりました。
ああ、データベースダイアグラムは地獄からの変異スパイダーのように見えました。
POBIはどうですか-意図的に明らかに悲観化しますか?
90年代の私の同僚は、CEOがすべてのERPソフトウェア(カスタムリリース)のリリースの最初の日を新しい機能のパフォーマンスの問題の特定に費やしたという理由だけで、CEOに尻を蹴られることにうんざりしていました。新しい機能がギガバイトを使い果たし、不可能を可能にしたとしても、彼は常にいくつかの詳細、または一見大きな問題でさえも、不平を言うことに気づきました。彼はプログラミングについて多くのことを知っていると信じていて、プログラマーのお尻を蹴ることによって彼のキックを得ました。
批判の無能な性質(彼はITの人ではなくCEOであった)のために、私の同僚はそれを正しく理解することができませんでした。パフォーマンスの問題がない場合、それを排除することはできません...
1つのリリースまで、彼は多くの遅延(200)関数呼び出し(それはDelphiでした)を新しいコードに入れました。稼働開始からわずか20分後、彼はCEOのオフィスに出て、期限切れの侮辱を直接受け取るように命じられました。
これまでの唯一の珍しいことは、彼が戻って、笑顔で、冗談を言って、BigMacや2人でBigMacに出かけたとき、彼が通常テーブルを蹴り、CEOと会社について怒鳴り、その日の残りの時間を費やして死んだときの私の同僚のミュートでした。
当然のことながら、私の同僚は今や彼の机で1〜2日間休憩し、Quakeでの照準スキルを向上させました。2日目または3日目に、Delayコールを削除し、再構築して「緊急パッチ」をリリースしました。彼がパフォーマンスの穴を修正するために2日と1泊を費やしたこと。
これは、邪悪なCEOが「素晴らしい仕事だ!」と言ったのは初めて(かつ唯一)でした。彼に。それだけが重要ですよね?
これは本物のPOBIでした。
しかし、これは一種のソーシャルプロセスの最適化でもあるので、100%OKです。
おもう。
「データベースの独立性」。これは、ストアドプロシージャ、トリガーなどがなく、外部キーも含まれないことを意味します。
var stringBuilder = new StringBuilder();
stringBuilder.Append(myObj.a + myObj.b + myObj.c + myObj.d);
string cat = stringBuilder.ToString();
私が今まで見たStringBuilderの最良の使用法。
単純なstring.splitで十分なときに正規表現を使用して文字列を分割する
私が知っているこのスレッドには非常に遅れていますが、最近これを見ました:
bool isFinished = GetIsFinished();
switch (isFinished)
{
case true:
DoFinish();
break;
case false:
DoNextStep();
break;
default:
DoNextStep();
}
ええ、ブール値に追加の値がある場合に備えて...
私が考えることができる最悪の例は、すべての従業員に関する情報を含む私の会社の内部データベースです。それは人事から毎晩更新を取得し、ASP.NET Webサービスを上に持っています。他の多くのアプリは、Webサービスを使用して、検索/ドロップダウンフィールドなどを入力します。
悲観論は、開発者がWebサービスへの繰り返しの呼び出しは繰り返しSQLクエリを作成するには遅すぎると考えていたことです。それで、彼は何をしましたか?アプリケーション開始イベントはデータベース全体を読み取り、それをすべてメモリ内のオブジェクトに変換します。アプリケーションプールがリサイクルされるまで無期限に保存されます。このコードは非常に遅いため、2000人未満の従業員でロードするのに15分かかります。日中に誤ってアプリプールをリサイクルした場合、各Webサービス要求が複数の同時リロードを開始するため、30分以上かかることがあります。このため、新規採用者はアカウントが作成された最初の日にはデータベースに表示されないため、最初の2日間はほとんどの社内アプリにアクセスできず、親指をいじる必要がなくなります。
悲観論の2番目のレベルは、依存するアプリケーションの破壊を恐れて開発マネージャーがそれに触れたくないということですが、そのような単純なコンポーネントの設計が不十分なため、重要なアプリケーションが散発的に全社的に停止し続けています。
誰もソートについて言及していないようですので、私は述べます。
何度か、私は誰かがバブルソートを手動で作成したことを発見しました。これは、状況が、すでに存在する「ファンシーすぎる」クイックソートアルゴリズムの呼び出しを「必要としなかった」ためです。テストのために使用している10行のデータに対して、手作りのバブルソートが十分に機能したとき、開発者は満足しました。顧客が数千行を追加した後、それはあまりうまく行きませんでした。
私はかつて、次のようなコードでいっぱいのアプリに取り組みました:
1 tuple *FindTuple( DataSet *set, int target ) {
2 tuple *found = null;
3 tuple *curr = GetFirstTupleOfSet(set);
4 while (curr) {
5 if (curr->id == target)
6 found = curr;
7 curr = GetNextTuple(curr);
8 }
9 return found;
10 }
を削除し、最後found
に戻っnull
て、6行目を次のように変更します。
return curr;
アプリのパフォーマンスが2倍になりました。
私はかつてこれらの宝石を定数クラスに含むコードを変更する必要がありました
public static String COMMA_DELIMINATOR=",";
public static String COMMA_SPACE_DELIMINATOR=", ";
public static String COLIN_DELIMINATOR=":";
これらのそれぞれは、アプリケーションの残りの部分でさまざまな目的で複数回使用されました。COMMA_DELIMINATORは、8つの異なるパッケージで200以上の使用でコードを散らかしました。
私のCコンパイラのオプティマイザとルーチンの裏をかくようとしている同僚がいて、彼だけが読むことができるコードを書き直しました。彼のお気に入りのトリックの1つは、(いくつかのコードを作成する)のような読み取り可能なメソッドを変更することでした:
int some_method(int input1, int input2) {
int x;
if (input1 == -1) {
return 0;
}
if (input1 == input2) {
return input1;
}
... a long expression here ...
return x;
}
これに:
int some_method() {
return (input == -1) ? 0 : (input1 == input2) ? input 1 :
... a long expression ...
... a long expression ...
... a long expression ...
}
つまり、一度読み取り可能なメソッドの最初の行は " return
"になり、他のすべてのロジックは深くネストされた3項式に置き換えられます。これがどのように維持不可能であるかについて議論しようとしたとき、彼は彼のメソッドのアセンブリ出力が3または4つのアセンブリ命令よりも短いという事実を指摘しました。必ずしも速くはありませんでしたが、常に小さなものでした少し短いです。これは、時々メモリ使用量が問題になる組み込みシステムでしたが、コードを読み取り可能にしておくよりもはるかに簡単な最適化を行うことができました。
そして、この後、なんらかの理由でptr->structElement
判読不能と判断したため、これらすべてを(*ptr).structElement
より読みやすく、より高速であるという理論に変え始めました。
読み取り可能なコードを読み取り不能なコードに変換して、最大で1%の改善を実現します。
if
ます。Cでの表現に関する主張の主張は、文化的/宗教的な教義であり、いかなる客観的な慣習でもありません。(より良いガイドライン:入れ子になった3項が長すぎて読みにくい場合は、if
どちらも使用しないでください。)
if
関数でシングルを取り、それを三項で置き換えることについて話しているのではありません。それは問題なく、多くの場合より読みやすくなっています。30行以上のメソッド全体を単一のreturnステートメントとネストされた3値に置き換えることについて話している。新しいコードの方が読みやすいとは誰も思っていませんでしたが、1人の開発者はそれがより速いと思っていました。
私は本格的な開発者としての最初の仕事の1つで、スケーリングの問題に悩まされていたプログラムのプロジェクトを引き継ぎました。小さなデータセットでは十分に機能しますが、大量のデータが与えられると完全にクラッシュします。
調べてみると、元のプログラマーは分析を並列化することでスピードアップを図り、追加のデータソースごとに新しいスレッドを起動しようとしていることがわかりました。ただし、すべてのスレッドが共有リソースを必要とし、その上でデッドロックが発生していたため、彼は間違いを犯していました。もちろん、同時実行のすべての利点は消えました。さらに、100以上のスレッドを起動するためにほとんどのシステムをクラッシュさせ、そのうちの1つを除くすべてをロックしました。私の頑丈な開発マシンは例外で、約6時間で150のソースデータセットを処理しました。
そこで、それを修正するために、マルチスレッドコンポーネントを削除し、I / Oをクリーンアップしました。他の変更はありませんでしたが、150ソースのデータセットの実行時間は私のマシンでは10分未満になり、平均的な会社のマシンでは無限から30分未満になりました。
私はこの宝石を提供できると思います:
unsigned long isqrt(unsigned long value)
{
unsigned long tmp = 1, root = 0;
#define ISQRT_INNER(shift) \
{ \
if (value >= (tmp = ((root << 1) + (1 << (shift))) << (shift))) \
{ \
root += 1 << shift; \
value -= tmp; \
} \
}
// Find out how many bytes our value uses
// so we don't do any uneeded work.
if (value & 0xffff0000)
{
if ((value & 0xff000000) == 0)
tmp = 3;
else
tmp = 4;
}
else if (value & 0x0000ff00)
tmp = 2;
switch (tmp)
{
case 4:
ISQRT_INNER(15);
ISQRT_INNER(14);
ISQRT_INNER(13);
ISQRT_INNER(12);
case 3:
ISQRT_INNER(11);
ISQRT_INNER(10);
ISQRT_INNER( 9);
ISQRT_INNER( 8);
case 2:
ISQRT_INNER( 7);
ISQRT_INNER( 6);
ISQRT_INNER( 5);
ISQRT_INNER( 4);
case 1:
ISQRT_INNER( 3);
ISQRT_INNER( 2);
ISQRT_INNER( 1);
ISQRT_INNER( 0);
}
#undef ISQRT_INNER
return root;
}
平方根は非常に敏感な場所で計算されたので、それをより速くする方法を検討するという仕事を得ました。この小さなリファクタリングにより、実行時間が3分の1に短縮されました(使用するハードウェアとコンパイラーの組み合わせ、YMMVの場合):
unsigned long isqrt(unsigned long value)
{
unsigned long tmp = 1, root = 0;
#define ISQRT_INNER(shift) \
{ \
if (value >= (tmp = ((root << 1) + (1 << (shift))) << (shift))) \
{ \
root += 1 << shift; \
value -= tmp; \
} \
}
ISQRT_INNER (15);
ISQRT_INNER (14);
ISQRT_INNER (13);
ISQRT_INNER (12);
ISQRT_INNER (11);
ISQRT_INNER (10);
ISQRT_INNER ( 9);
ISQRT_INNER ( 8);
ISQRT_INNER ( 7);
ISQRT_INNER ( 6);
ISQRT_INNER ( 5);
ISQRT_INNER ( 4);
ISQRT_INNER ( 3);
ISQRT_INNER ( 2);
ISQRT_INNER ( 1);
ISQRT_INNER ( 0);
#undef ISQRT_INNER
return root;
}
もちろん、これを行うにはより速くより良い方法がありますが、私はそれが悲観化のかなりきちんとした例だと思います。
編集:考えてみてください。展開されたループは、実際にはきちんとした悲観化でもありました。バージョンコントロールを掘り下げて、リファクタリングの第2ステージも紹介できます。
unsigned long isqrt(unsigned long value)
{
unsigned long tmp = 1 << 30, root = 0;
while (tmp != 0)
{
if (value >= root + tmp) {
value -= root + tmp;
root += tmp << 1;
}
root >>= 1;
tmp >>= 2;
}
return root;
}
これは、実装が少し異なりますが、まったく同じアルゴリズムなので、適格であると思います。
isqrt()
を計算しfloor(sqrt())
、しかし、なぜこのコードの動作はしますか?
これはあなたが求めていたものよりも高いレベルにあるかもしれませんが、それを修正する(許可されている場合)には、より高いレベルの痛みも伴います。
確立され、テストされ、成熟したライブラリの1つを使用する代わりに、オブジェクトリレーションシップマネージャー/データアクセスレイヤーを手作業でローリングすることを主張する(たとえそれらがあなたに指摘された後でも)。
これは問題に完全には適合しませんが、とにかく警告的な話をします。私は、実行速度が遅い分散アプリで作業していて、問題を解決することを主な目的とする会議に参加するためにDCに飛んで行きました。プロジェクトリーダーは、遅延の解決を目的とした再アーキテクチャの概要を説明し始めました。週末にいくつかの測定を行って、ボトルネックを単一の方法に限定したことを申し出ました。ローカルルックアップでレコードが欠落しており、アプリケーションがトランザクションごとにリモートサーバーにアクセスする必要があることが判明しました。レコードをローカルストアに追加することで、遅延が解消され、問題は解決しました。再構築によって問題が修正されなかったことに注意してください。
すべてのJavaScript操作の前に、操作対象のオブジェクトが存在するかどうかを確認します。
if (myObj) { //or its evil cousin, if (myObj != null) {
label.text = myObj.value;
// we know label exists because it has already been
// checked in a big if block somewhere at the top
}
このタイプのコードに関する私の問題は、それが存在しない場合、誰も何も気にしないようです?何もしない?ユーザーにフィードバックを提供しませんか?
Object expected
エラーが煩わしいことに同意しますが、これはそのための最良の解決策ではありません。
YAGNI過激主義はどうでしょう。それは時期尚早の悲観化の一形態です。YAGNIを適用するといつでもそれが必要になり、最初に追加した場合よりも10倍の労力が必要になります。成功したプログラムを作成した場合、オッズはそれを必要としています。人生がすぐに尽きるプログラムを作成することに慣れているなら、YAGNIを練習することを続けます。
これは時期尚早な最適化ではありませんが、間違いですが、これはBBCのWebサイトでWindows 7についての記事から読まれました。
カラン氏は、Microsoft Windowsチームは、オペレーティングシステムのあらゆる面を徹底的に改善してきたと語った。「WAVファイルのシャットダウン音楽をわずかにトリミングすることで、シャットダウン時間を400ミリ秒短縮することができました。
現在、私はまだWindows 7を試していないので、間違っているかもしれませんが、シャットダウンにかかる時間よりも重要な他の問題があることに間違いはありません。結局のところ、「Windowsをシャットダウンしています」というメッセージが表示されると、モニターの電源がオフになり、離れていきます。この400ミリ秒のメリットは何ですか?
私の部門の誰かがかつて文字列クラスを書いたことがあります。のようなインターフェースCString
が、Windowsに依存していません。
彼らが行った「最適化」の1つは、必要以上のメモリを割り当てないことでした。どうやらクラスがstd::string
過剰なメモリを割り当てる理由に気付かないのは、+=
操作をO(n)時間で実行できる。
代わりに、1回の+=
呼び出しごとに再割り当てが強制され、追加が繰り返されてO(n²)ペインターのアルゴリズムになりました。
私の元同僚(実際にはsoab)が割り当てられ、顧客のデータ(小売業界)を収集および分析する必要があるJava ERPの新しいモジュールを構築しました。彼は、EVERY Calendar / Datetimeフィールドをそのコンポーネント(秒、分、時間、日、月、年、曜日、妊娠中期、妊娠中期(!))に分割することを決定しました。
誰にも害はありませんが、私はこれを持っている課題(java)を採点しただけです
import java.lang.*;