2


1

このLINQを高速化する方法はありますか?

私はアプリケーションを遅くしているLINQ式を持っています。 コントロールを描画していますが、これを行うには、列に表示されるテキストの最大幅を知る必要があります。

私がそれをしている方法はこれです:

return Items.Max(w => TextRenderer.MeasureText((w.RenatlUnit == null)? "" :
w.RenatlUnit.UnitNumber, this.Font).Width) + 2;

ただし、これは〜1000アイテム以上繰り返され、描画メソッドで使用されるCPU時間の約20%を消費します。 さらに悪いことに、これを行う必要のある他の2つの列があるため、すべてのアイテム/列に対するこのLINQステートメントには、CPU時間の75〜85%がかかります。

TextRendererはSystem.Windows.Formsパッケージに由来し、モノスペースフォントを使用していないため、文字列のピクセル幅を把握するにはMeasureTextが必要です。

これをどのように高速化できますか?

7 Answer


6


私はあなたの問題がLINQの速度にあるとは思わない。それはあなたが1000回以上「MeasureText」を呼び出しているという事実にある。 LINQクエリからロジックを取り出して通常の「foreach」ループに入れると、同様の実行時間が得られると思います。

より良いアイデアは、おそらくあなたがやっていることの周りに少し健全性チェックを採用することです。 合理的な入力で行けば(そして改行の可能性を無視して)、本当に文字列のテキストを測定する必要があるのは、たとえば文字列の絶対的な最長文字列の10%程度以内である、その後、最大値を使用します。 つまり、最大値が「古生物学」である場合、文字列「foo」を測定しても意味がありません。 幅がTHAT変数のフォントはありません。


6


時間がかかるのは `MeasureText`メソッドであるため、速度を上げる唯一の方法は作業を減らすことです。

`MeasureText`の呼び出しの結果を辞書にキャッシュできます。これにより、以前に測定された文字列を再測定する必要がなくなります。

値を一度計算すると、表示するデータと一緒に保持できます。 データを変更するたびに、値を再計算します。 そうすれば、コントロールが描画されるたびに文字列を測定する必要がなくなります。


4


ステップ0: _Profile._実行時間のほとんどが実際に `MeasureText`にあるとわかった場合、次のことを試して呼び出し回数を減らすことができます。

  1. すべての個々の文字の長さを計算します。 のように聞こえるので あなたは数字をレンダリングしている、これは小さなセットでなければなりません。

  2. 長さを見積もる numstr.Select(digitChar ⇒ digitLengthDict [digitChar])。Sum()

  3. 一番上の_N_の長さの文字列を取り、それらだけを測定します。

  4. lookup + sumのコストの大部分を回避するために、フィルタリングも 提案されているように、最大​​文字列長の90%以内の文字列のみを含めます。

e.g. 何かのようなもの…​

// somewhere else, during initialization - do only once.
var digitLengthDict = possibleChars.ToDictionary(c=>c,c=>TextRenderer.MeasureText(c.ToString()));

//...

var relevantStringArray = Items.Where(w=>w.RenatlUnit!=null).Select(w.RenatlUnit.UnitNumber).ToArray();

double minStrLen = 0.9*relevantStringArray.Max(str => str.Length);

return (
    from numstr in relevantStringArray
    where str.Length >= minStrLen
    orderby numstr.Select(digitChar=>digitLengthDict[digitChar]).Sum() descending
    select TextRenderer.MeasureText(numstr)
    ).Take(10).Max() + 2;

文字列の分布についてもっと知っていれば、それは助けになるでしょう。

また、「MeasureText」は魔法ではありません。限られた入力セットに対して、その機能を非常に簡単に複製できる可能性は十分にあります。 たとえば、文字列の測定された長さが、文字列内のすべての文字の長さの合計から文字列内のすべての文字のバイグラムのカーニングオーバーハングを引いたものに正確に等しいことを知っても驚かないでしょう。 文字列が、たとえば「0」-「9」、「+」、「-」、「、」、「。」、およびターミネータ記号で構成される場合、14文字幅と「15 *」のルックアップテーブル15-1`カーネル修正は、 `MeasureText`をはるかに高速で、それほど複雑にせずに正確にエミュレートするのに十分かもしれません。

最後に、最善の解決策は、問題をまったく解決しないことです。おそらく、このような正確な数を必要としないようにアプリケーションを再設計できます。 - より簡単な見積もりで十分な場合は、「MeasureText」を避けることができます ほぼ完全に。


1


残念ながら、LINQが問題のようには見えません。 「for」ループを実行してこの同じ計算を行った場合、時間は同じ桁になります。

この計算を複数のスレッドで実行することを検討しましたか? Parallel LINQでうまく動作します。

編集:「MeasureText」はGDI関数であり、単にUIスレッドにマーシャリングされるため、Parallel LINQは動作しないようです(@Adam Robinsonが私を修正してくれたことに感謝します)


1


私の推測では、問題はLINQ式ではなく、「MeasureText」を数千回呼び出すことです。

問題を4つの部分に分けることで、モノスペースでないフォントの問題を回避できると思います。

  1. レンダリングサイズに関して最大​​数を見つける

  2. 最も数字の多いアパートを見つける

  3. すべての値が#1で決定された値である文字列を作成し、 #2のサイズを持ちます。

  4. #3で作成した値をMeasureTextに渡し、それを基礎として使用します

これは完璧な解決策にはなりませんが、アイテムに少なくとも十分なスペースを確保し、「MeasureText」を何度も呼び出すという落とし穴を避けることができます。


0


「MeasureText」を高速化する方法がわからない場合は、フォントサイズとスタイル内のすべての文字の幅を事前に計算し、そのような文字列の幅を見積もることができますが、文字ペアのカーニングはおそらく推定値であり、正確ではありません。


0


最も長い文字列の長さを取得し、その長さの「0」の文字列の幅(または最も広い桁が何であれ、私は覚えていない)を見つける近似として考慮することをお勧めします。 それははるかに高速な方法であるはずですが、それは近似に過ぎず、おそらく必要以上に長いでしょう。

var longest = Items.Max( w => w.RenatlUnit == null
                                  || w.RenatlUnit.UnitNumber == null)
                              ? 0
                              : w.RenatlUnit.UnitNumber.Length );
if (longest == 0)
{
    return 2;
}
return TextRenderer.MeasureText( new String('0', longest ) ).Width + 2;