パフォーマンスホットスポットであると測定したsprintfを改善/交換するにはどうすればよいですか?
-
07-07-2019 - |
質問
プロファイリングにより、ここでのsprintfには時間がかかることがわかりました。 y / m / d h / m / sフィールドの先行ゼロを引き続き処理する、より優れたパフォーマンスの代替手段はありますか?
SYSTEMTIME sysTime;
GetLocalTime( &sysTime );
char buf[80];
for (int i = 0; i < 100000; i++)
{
sprintf(buf, "%4d-%02d-%02d %02d:%02d:%02d",
sysTime.wYear, sysTime.wMonth, sysTime.wDay,
sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
}
注:OPはコメントで、これは簡略化された例であると説明しています。 「本物」ループには、データベースのさまざまな時間値を使用する追加コードが含まれています。プロファイリングにより、 sprintf()
が攻撃者として特定されました。
解決
ジョブを実行する独自の関数を作成している場合、0〜61の文字列値のルックアップテーブルを使用すると、年以外のすべての演算を行う必要がなくなります。
編集:うるう秒に対処するために(および strftime ()
)60と61の秒値を印刷できるはずです。
char LeadingZeroIntegerValues[62][] = { "00", "01", "02", ... "59", "60", "61" };
また、 strftime()
?パフォーマンスがどのように比較されるかわかりません(sprintf()を呼び出すだけでもかまいません)が、見る価値はあります(また、上記のルックアップ自体を行うこともできます)。
他のヒント
出力の各文字を順番に埋めることができます。
buf[0] = (sysTime.wYear / 1000) % 10 + '0' ;
buf[1] = (sysTime.wYear / 100) % 10 + '0';
buf[2] = (sysTime.wYear / 10) % 10 + '0';
buf[3] = sysTime.wYear % 10 + '0';
buf[4] = '-';
...など...
見た目はよくありませんが、写真が得られます。それ以外の場合は、sprintfがそれほど高速にならない理由を説明するのに役立ちます。
OTOH、最後の結果をキャッシュすることができます。そうすれば、毎秒生成するだけで済みます。
Printfは、多くの異なる形式に対処する必要があります。 確かに printfのソースを取得して、ロールのベースとして使用できます。 sysTime 構造を特に扱う独自のバージョン。そうすれば、1つの引数を渡すだけで、実行する必要のある作業だけが実行され、それ以上の処理は行われません。
「長い」とはどういう意味ですか?時間- sprintf()
がループ内の唯一のステートメントであり、&quot;配管&quot;ループの(増分、比較)は無視でき、 sprintf()
が最も時間を消費します。
ある夜、3番街で結婚指輪を失った男についての古いジョークを思い出してください。 「証明」するように設計された例を作成しました。 sprintf()
は非効率的であるという仮定。
「実際の」プロファイルを作成すると、結果がより正確になります。使用する他のすべての関数とアルゴリズムに加えて、 sprintf()
を含むコード。または、必要な特定のゼロ埋め込み数値変換に対応する独自のバージョンを作成してみてください。
結果に驚くかもしれません。
Jaywalkerが非常によく似た方法を提案しているようです(1時間以内に私を叩きます)。
すでに提案されているルックアップテーブルメソッド(以下のn2s []配列)に加えて、通常のsprintfの負荷を軽減するためにフォーマットバッファを生成する方法はありますか?以下のコードは、年/月/日/時が変更されない限り、ループを通るたびに分と秒を入力するだけです。明らかに、それらのいずれかが変更された場合、別のsprintfヒットを取得しますが、全体的には、現在目撃しているものを超えない可能性があります(配列ルックアップと組み合わせた場合)。
static char fbuf[80];
static SYSTEMTIME lastSysTime = {0, ..., 0}; // initialize to all zeros.
for (int i = 0; i < 100000; i++)
{
if ((lastSysTime.wHour != sysTime.wHour)
|| (lastSysTime.wDay != sysTime.wDay)
|| (lastSysTime.wMonth != sysTime.wMonth)
|| (lastSysTime.wYear != sysTime.wYear))
{
sprintf(fbuf, "%4d-%02s-%02s %02s:%%02s:%%02s",
sysTime.wYear, n2s[sysTime.wMonth],
n2s[sysTime.wDay], n2s[sysTime.wHour]);
lastSysTime.wHour = sysTime.wHour;
lastSysTime.wDay = sysTime.wDay;
lastSysTime.wMonth = sysTime.wMonth;
lastSysTime.wYear = sysTime.wYear;
}
sprintf(buf, fbuf, n2s[sysTime.wMinute], n2s[sysTime.wSecond]);
}
結果をキャッシュする方法は?それは可能性ではありませんか?この特定のsprintf()呼び出しがコード内であまりにも頻繁に行われることを考えると、これらの連続した呼び出しのほとんどの間で、年、月、日は変わらないと想定しています。
したがって、次のようなものを実装できます。古いおよび現在のSYSTEMTIME構造体を宣言します。
SYSTEMTIME sysTime, oldSysTime;
また、日付と時刻を保持する個別の部分を宣言します:
char datePart[80];
char timePart[80];
最初に、sysTime、oldSysTime、datePartおよびtimePartの両方を入力する必要があります。ただし、次のように、後続のsprintf()を非常に高速化できます。
sprintf (timePart, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond); if (oldSysTime.wYear == sysTime.wYear && oldSysTime.wMonth == sysTime.wMonth && oldSysTime.wDay == sysTime.wDay) { // we can reuse the date part strcpy (buff, datePart); strcat (buff, timePart); } else { // we need to regenerate the date part as well sprintf (datePart, "%4d-%02d-%02d", sysTime.wYear, sysTime.wMonth, sysTime.wDay); strcpy (buff, datePart); strcat (buff, timePart); } memcpy (&oldSysTime, &sysTime, sizeof (SYSTEMTIME));
上記のコードには、コードを理解しやすくするための冗長性があります。簡単にファクタリングできます。時間と分でさえルーチンへの呼び出しよりも速く変化しないことがわかっている場合は、さらに高速化できます。
いくつかのことをします...
- 現在の時刻をキャッシュして、毎回タイムスタンプを再生成する必要がないようにします
- 時間変換を手動で行います。
printf
ファミリ関数の最も遅い部分はformat-string解析であり、ループの実行ごとにその解析にサイクルを当てるのは愚かなことです。 - すべての変換に2バイトのルックアップテーブルを使用してみてください(
{&quot; 00&quot;、&quot; 01&quot;、&quot; 02&quot;、...、&quot; 99&quot;}
)。これは、モジュラー算術を避けたいためであり、2バイトのテーブルは、1年間にモジュロを1つだけ使用する必要があることを意味します。
フォーマット文字列の繰り返し解析を回避でき、sprintfハンドルの多くの複雑なケースを処理する必要がないため、戻りbufの数字をレイアウトするルーチンを手動でローリングすることにより、おそらくw perfが増加します。 。しかし、実際にそれを行うことをお勧めします。
これらの文字列を生成するために必要な量を何らかの方法で減らすことができるかどうかを把握することをお勧めします。
現在、同様の問題に取り組んでいます。
組み込みシステムで、タイムスタンプ、ファイル名、行番号などを含むデバッグステートメントを記録する必要があります。ロガーはすでに配置されていますが、ノブを「フルロギング」にすると、すべてのprocサイクルが消費され、システムが悲惨な状態になります。コンピューティングデバイスは経験する必要がありません。
誰かが「あなたが測定/観察しているものを変更せずに何かを測定/観察することはできない」と言った
だから、パフォーマンスを改善するために物事を変えています。現在の状況では、Imは元の関数呼び出しよりも2倍高速です(そのロギングシステムのボトルネックは関数呼び出しではなく、個別の実行可能ファイルであるログリーダーにあります。独自のログスタックを作成する場合)。
提供する必要があるインターフェイスは、 void log(int channel、char * filename、int lineno、format、...)
のようなものです。チャネル名(現在、リスト内で線形検索を実行しています!デバッグステートメントごとに)とミリ秒カウンターを含むタイムスタンプを追加する必要があります。これを高速化するために私がやっていることのいくつかを以下に示します。
- リストを検索するのではなく、
strcpy
できるようにチャンネル名を文字列化します。マクロLOG(channel、... etc)
をlog(#channel、... etc)
として定義します。LOG(channel、...)
log(&quot; ....&quot;#を定義して文字列の長さを修正する場合、
固定 10 バイトチャネル長memcpy
を使用できます。 channel-sizeof(&quot; ....&quot; #channel)+ * 11 *) - タイムスタンプ文字列を1秒間に2、3回生成します。 asctimeなどを使用できます。次に、すべてのデバッグステートメントに固定長文字列をmemcpyします。
- リアルタイムでタイムスタンプ文字列を生成したい場合、memcpyではなく、割り当てのあるルックアップテーブルが最適です。ただし、これは2桁の数字でのみ機能し、おそらく1年で機能します。
-
3桁(ミリ秒)と5桁(lineno)はどうですか? divとmodは slow なので、私はitoaが好きではなく、カスタムitoa(
digit =((value / = value)%10)
)も好きではありません。以下の関数を書いた後、AMD最適化マニュアル(アセンブリー)に類似したものがあることを発見しました。これにより、これらは最速のC実装に関するものであると確信できます。void itoa03(char *string, unsigned int value) { *string++ = '0' + ((value = value * 2684355) >> 28); *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28); *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28); *string++ = ' ';/* null terminate here if thats what you need */ }
同様に、行番号については、
void itoa05(char *string, unsigned int value) { *string++ = ' '; *string++ = '0' + ((value = value * 26844 + 12) >> 28); *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28); *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28); *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28); *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28); *string++ = ' ';/* null terminate here if thats what you need */ }
全体として、私のコードは今ではかなり高速です。使用する必要がある vsnprintf()
の時間は約91%で、残りのコードはわずか9%しかかかりません(一方、残りのコードは vsprintf()
以前は54%早く取得していました)
テストした2つの高速フォーマッターは、 FastFormat と Karma :: generate (ブーストスピリット)。
また、ベンチマークを行うか、少なくとも既存のベンチマークを探すことも有用かもしれません。
たとえば、これ(FastFormatはありませんが):
StringStreamは、Googleから得た提案です。
整数の書式設定でsprintfを破ることは想像しにくいです。 sprintfが問題ですか?