memset(ptr, 0, nbytes)
は本当に速いことを学びましたが、より速い方法があります(少なくともx86で)。
Memsetはmov
を使用すると仮定しますが、メモリをゼロ化する場合、ほとんどのコンパイラはxor
を使用します。 edit1:間違っています。GregSがレジスタでのみ機能すると指摘したためです。私が考えていたことは何でしょう?
また、私よりもアセンブラーを知っている人にstdlibを見てもらい、x86ではmemsetが32ビット幅のレジスターを十分に活用していないと教えてくれました。しかし、その時はとても疲れていたので、正しく理解できたかどうかはわかりません。
edit2:この問題を再検討し、少しテストを行いました。私がテストしたものは次のとおりです。
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <sys/time.h>
#define TIME(body) do { \
struct timeval t1, t2; double elapsed; \
gettimeofday(&t1, NULL); \
body \
gettimeofday(&t2, NULL); \
elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; \
printf("%s\n --- %f ---\n", #body, elapsed); } while(0) \
#define SIZE 0x1000000
void zero_1(void* buff, size_t size)
{
size_t i;
char* foo = buff;
for (i = 0; i < size; i++)
foo[i] = 0;
}
/* I foolishly assume size_t has register width */
void zero_sizet(void* buff, size_t size)
{
size_t i;
char* bar;
size_t* foo = buff;
for (i = 0; i < size / sizeof(size_t); i++)
foo[i] = 0;
// fixes bug pointed out by tristopia
bar = (char*)buff + size - size % sizeof(size_t);
for (i = 0; i < size % sizeof(size_t); i++)
bar[i] = 0;
}
int main()
{
char* buffer = malloc(SIZE);
TIME(
memset(buffer, 0, SIZE);
);
TIME(
zero_1(buffer, SIZE);
);
TIME(
zero_sizet(buffer, SIZE);
);
return 0;
}
結果:
zero_1は、-O3を除いて最も低速です。 zero_sizetは、-O1、-O2、および-O3のパフォーマンスがほぼ等しい最速です。 memsetは常にzero_sizetよりも低速でした。 (-O3では2倍遅い)。興味深いのは、-O3でzero_1がzero_sizetと同等に高速だったことです。ただし、逆アセンブルされた関数には、約4倍の命令がありました(ループの展開が原因だと思います)。また、zero_sizetをさらに最適化しようとしましたが、コンパイラは常に私を上回っていましたが、ここでは驚きではありませんでした。
今のところmemsetが勝ち、以前の結果はCPUキャッシュによって歪められました。 (すべてのテストはLinuxで実行されました)さらにテストが必要です。次にアセンブラーを試してみます:)
edit3:テストコードのバグを修正、テスト結果は影響を受けません
edit4:逆アセンブルされたVS2010 Cランタイムを調べていると、memset
にはSSEゼロ用に最適化されたルーチンがあります。この。
x86はかなり幅広いデバイスです。
完全に汎用のx86ターゲットの場合、「rep movsd」を含むAssemblyブロックは、32ビットのメモリにゼロを一度に吹き飛ばす可能性があります。この作業の大部分がDWORDに揃えられていることを確認してください。
Mmxを備えたチップの場合、movqを備えたアセンブリループは一度に64ビットをヒットする可能性があります。
C/C++コンパイラで、long longまたは_m64へのポインタで64ビットの書き込みを使用できる場合があります。最高のパフォーマンスを得るには、ターゲットを8バイトに揃える必要があります。
sseを備えたチップの場合、movapsは高速ですが、アドレスが16バイトにアライメントされている場合にのみ、movsbをアライメントされるまで使用してから、movapsのループでクリアを完了します。
Win32には「ZeroMemory()」がありますが、memsetのマクロか、実際の「良い」実装かを忘れています。
memset
は通常、非常に高速になるように設計されています汎用設定/ゼロ化コード。サイズや配置が異なるすべてのケースを処理します。これは、作業の実行に使用できる命令の種類に影響します。使用しているシステム(およびstdlibの提供元のベンダー)に応じて、基礎となる実装は、そのアーキテクチャ固有のアセンブラー内にあり、ネイティブプロパティが何であれそれを利用します。また、ゼロ化のケースを処理するための内部の特別なケースがある場合があります(他の値を設定する場合とは異なります)。
とはいえ、非常に具体的で非常にパフォーマンスが重要なメモリゼロ化を行う必要がある場合、特定のmemset
実装を自分でやることで勝てる可能性は確かにあります。 memset
と標準ライブラリ内のその友人は、常に1人用プログラミングの楽しいターゲットです。 :)
最近では、コンパイラーがすべての作業を行う必要があります。少なくとも私の知る限り、gccはmemset
の呼び出しを最適化するのに非常に効率的です(ただし、アセンブラを確認する方がよいでしょう)。
また、必要がない場合はmemset
も避けてください:
... = { 0 }
)スタックメモリ用本当に大きなチャンクの場合は、mmap
を使用します。これは、システムからゼロの初期化メモリを「無料で」取得するだけです。
特定のニーズがある場合、またはコンパイラー/ stdlibが悪臭を放つことがわかっている場合を除き、memsetを使用してください。それは汎用であり、一般的にまともなパフォーマンスを発揮するはずです。また、コンパイラはmemset()を最適化/インライン化する方が簡単な場合があります。これは、コンパイラが本質的にサポートしているためです。
たとえば、Visual C++は多くの場合、ライブラリ関数に対して呼び出しと同じくらい小さいであるmemcpy/memsetのインラインバージョンを生成し、プッシュ/呼び出し/再試行のオーバーヘッドを回避します。コンパイル時にサイズパラメータを評価できる場合、さらに可能な最適化があります。
ただし、specificのニーズがある場合(サイズは常にtiny*です)または*巨大)、アセンブリレベルにドロップダウンすることで速度を上げることができます。たとえば、ライトスルー操作を使用して、L2キャッシュを汚染することなく、巨大なメモリチャンクをゼロにします。
しかし、それはすべて依存します-そして、通常のものについては、memset/memcpyに固執してください:)
(数年前から)正しく覚えていれば、上級開発者の1人がPowerPCでbzero()をすばやく実行する方法について話していました(仕様では、電源投入時にほとんどすべてのメモリをゼロにする必要があると言われました)。 x86にうまく変換されない場合がありますが、調査する価値はあります。
そのアイデアは、データキャッシュラインをロードし、そのデータキャッシュラインをクリアしてから、クリアされたデータキャッシュラインをメモリに書き戻すことでした。
それが価値があるもののために、私はそれが役立つことを願っています。
memset
と= { 0 }
の比較については、質問 配列0-初期化からの奇妙なアセンブリ も参照してください。
それは興味深い質問です。この実装は、VC++ 2012で32ビットリリースをコンパイルするときにわずかに高速ですが(ほとんど測定できません)、おそらく大幅に改善される可能性があります。マルチスレッド環境のmemset()
でボトルネックの問題が報告されているため、これをマルチスレッド環境の独自のクラスに追加すると、パフォーマンスがさらに向上する可能性があります。
_// MemsetSpeedTest.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include "Windows.h"
#include <time.h>
#pragma comment(lib, "Winmm.lib")
using namespace std;
/** a signed 64-bit integer value type */
#define _INT64 __int64
/** a signed 32-bit integer value type */
#define _INT32 __int32
/** a signed 16-bit integer value type */
#define _INT16 __int16
/** a signed 8-bit integer value type */
#define _INT8 __int8
/** an unsigned 64-bit integer value type */
#define _UINT64 unsigned _INT64
/** an unsigned 32-bit integer value type */
#define _UINT32 unsigned _INT32
/** an unsigned 16-bit integer value type */
#define _UINT16 unsigned _INT16
/** an unsigned 8-bit integer value type */
#define _UINT8 unsigned _INT8
/** maximum allo
wed value in an unsigned 64-bit integer value type */
#define _UINT64_MAX 18446744073709551615ULL
#ifdef _WIN32
/** Use to init the clock */
#define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency);
/** Use to start the performance timer */
#define TIMER_START QueryPerformanceCounter(&t1);
/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl;
#else
/** Use to init the clock */
#define TIMER_INIT clock_t start;double diff;
/** Use to start the performance timer */
#define TIMER_START start=clock();
/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl;
#endif
void *MemSet(void *dest, _UINT8 c, size_t count)
{
size_t blockIdx;
size_t blocks = count >> 3;
size_t bytesLeft = count - (blocks << 3);
_UINT64 cUll =
c
| (((_UINT64)c) << 8 )
| (((_UINT64)c) << 16 )
| (((_UINT64)c) << 24 )
| (((_UINT64)c) << 32 )
| (((_UINT64)c) << 40 )
| (((_UINT64)c) << 48 )
| (((_UINT64)c) << 56 );
_UINT64 *destPtr8 = (_UINT64*)dest;
for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll;
if (!bytesLeft) return dest;
blocks = bytesLeft >> 2;
bytesLeft = bytesLeft - (blocks << 2);
_UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx];
for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll;
if (!bytesLeft) return dest;
blocks = bytesLeft >> 1;
bytesLeft = bytesLeft - (blocks << 1);
_UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx];
for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll;
if (!bytesLeft) return dest;
_UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx];
for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll;
return dest;
}
int _tmain(int argc, _TCHAR* argv[])
{
TIMER_INIT
const size_t n = 10000000;
const _UINT64 m = _UINT64_MAX;
const _UINT64 o = 1;
char test[n];
{
cout << "memset()" << endl;
TIMER_START;
for (int i = 0; i < m ; i++)
for (int j = 0; j < o ; j++)
memset((void*)test, 0, n);
TIMER_STOP;
}
{
cout << "MemSet() took:" << endl;
TIMER_START;
for (int i = 0; i < m ; i++)
for (int j = 0; j < o ; j++)
MemSet((void*)test, 0, n);
TIMER_STOP;
}
cout << "Done" << endl;
int wait;
cin >> wait;
return 0;
}
_
32ビットシステムのリリースコンパイル時の出力は次のとおりです。
_memset() took:
5.569000
MemSet() took:
5.544000
Done
_
64ビットシステム用のリリースコンパイル時の出力は次のとおりです。
_memset() took:
2.781000
MemSet() took:
2.765000
Done
_
ここで見つけることができます ソースコードBerkleyのmemset()
は、最も一般的な実装だと思います。
それ以外の場合、これは非常に役立つテストに致命的な欠陥が1つあります。memsetが最初の命令であるため、「メモリオーバーヘッド」などがあるため、非常に遅くなります。 memsetのタイミングを2位に、他の何かを1位に移動するか、memsetを2回タイミングするだけで、すべてのコンパイルスイッチでmemsetが最速になります!!!
Memset関数は、速度を犠牲にしても、柔軟でシンプルになるように設計されています。多くの実装では、指定されたバイト数で一度に1バイトずつ指定された値をコピーする単純なwhileループです。より高速なmemset(またはmemcpy、memmmoveなど)が必要な場合は、ほとんどの場合、自分で1つをコーディングすることができます。
最も簡単なカスタマイズは、宛先アドレスが32ビットまたは64ビットに整列するまで(チップのアーキテクチャに一致するまで)シングルバイトの「セット」操作を実行し、一度に完全なCPUレジスタのコピーを開始することです。範囲がアライメントされたアドレスで終了しない場合、最後にシングルバイトの「セット」操作をいくつか実行する必要があります。
特定のCPUに応じて、ストリーミングSIMD命令が役立つ場合があります。これらは通常、整列アドレスでより適切に機能するため、ここでも整列アドレスを使用する上記の手法が役立ちます。
メモリの大きなセクションをゼロにする場合、範囲をセクションに分割し、各セクションを並列処理することで速度が向上する場合があります(セクションの数は、数またはコア/ハードウェアスレッドと同じです)。
最も重要なことは、試してみないと、これが役立つかどうかを判断する方法がないことです。少なくとも、各ケースでコンパイラが出力するものを見てください。標準の「memset」に対して他のコンパイラが発行するものも参照してください(それらの実装は、コンパイラよりも効率的かもしれません)。
memsetは、コンパイラによって一連の効率的なオペコードとしてインライン化され、数サイクル展開されます。 4000x2000 64ビットフレームバッファーなどの非常に大きなメモリブロックの場合、それぞれが独自の部分を設定する複数のスレッド(その唯一のタスクのために準備する)で最適化を試みることができます。 bzero()もありますが、より不明瞭であり、memsetとして最適化される可能性が低いことに注意してください。コンパイラは、0を渡すことを確認します。
コンパイラが通常想定するのは、大きなブロックをmemsetすることです。したがって、小さなブロックの場合、多数の小さなオブジェクトを初期化する場合、*(uint64_t*)p = 0
を実行する方が効率的です。
通常、すべてのx86 CPUは異なり(標準化されたプラットフォーム用にコンパイルしない限り)、Pentium 2用に最適化したものはCore Duoまたはi486で異なる動作をします。そのため、本当に歯磨き粉の最後の数ビットを絞りたい場合は、exeをコンパイルして、さまざまな人気のあるCPUモデル向けに最適化したいくつかのバージョンを出荷するのが理にかなっています。個人的な経験から、Clang -march = nativeは、-marchを使用しない場合と比較して、ゲームのFPSを60から65に引き上げました。