これは、Microsoftが放棄した私たちFrameworkユーザー向けの別のバージョンです。Panos Theofのソリューション、Eric JとPetar Petrovの並列ソリューションの 4倍のArray.Clear
速度 4倍の速さで、大規模なアレイでは最大2倍高速です。
まず、関数の祖先を紹介します。これにより、コードが理解しやすくなります。パフォーマンスに関しては、これはPanos Theofのコードとほぼ同等であり、すでに十分な場合もあります。
public static void Fill<T> (T[] array, int count, T value, int threshold = 32)
{
if (threshold <= 0)
throw new ArgumentException("threshold");
int current_size = 0, keep_looping_up_to = Math.Min(count, threshold);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
for (int at_least_half = (count + 1) >> 1; current_size < at_least_half; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
ご覧のとおり、これはすでに初期化された部分を繰り返し2倍にすることに基づいています。これはシンプルで効率的ですが、最新のメモリアーキテクチャでは機能しません。したがって、ダブリングを使用してキャッシュフレンドリーなシードブロックを作成するバージョンが誕生しました。これは、ターゲット領域全体に繰り返しブラストされます。
const int ARRAY_COPY_THRESHOLD = 32; // 16 ... 64 work equally well for all tested constellations
const int L1_CACHE_SIZE = 1 << 15;
public static void Fill<T> (T[] array, int count, T value, int element_size)
{
int current_size = 0, keep_looping_up_to = Math.Min(count, ARRAY_COPY_THRESHOLD);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
int block_size = L1_CACHE_SIZE / element_size / 2;
int keep_doubling_up_to = Math.Min(block_size, count >> 1);
for ( ; current_size < keep_doubling_up_to; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
for (int enough = count - block_size; current_size < enough; current_size += block_size)
Array.Copy(array, 0, array, current_size, block_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
注:以前のコード(count + 1) >> 1
は、2倍のループの制限として必要でした。これにより、最後のコピー操作で、残っているすべてをカバーするのに十分な飼料が確保されます。count >> 1
代わりに使用する場合、これは奇数の場合には当てはまりません。現在のバージョンでは、線形コピーループがスラックを拾うため、これは重要ではありません。
-マインドゴーグル-ジェネリックは、将来利用可能になるか、または利用できなくなる可能性がsizeof
ある制約(unmanaged
)を使用しない限り、使用できません。そのため、配列セルのサイズをパラメーターとして渡す必要があります。次の理由により、間違った見積もりは大したことではありませんが、値が正確な場合にパフォーマンスが最高になります。
要素サイズを過小評価すると、ブロックサイズがL1キャッシュの半分よりも大きくなる可能性があるため、コピーソースデータがL1から追い出され、より遅いキャッシュレベルから再フェッチする必要が生じる可能性が高くなります。
要素サイズを過大に見積もると、CPUのL1キャッシュの使用率が低下します。つまり、線形ブロックコピーループは、最適な使用率よりも頻繁に実行されます。したがって、厳密に必要な量よりも多くの固定ループ/呼び出しオーバーヘッドが発生します。
ここに、私のコードを突き刺すベンチマークArray.Clear
と、前述した他の3つのソリューションがあります。タイミングはInt32[]
、指定されたサイズの整数配列()を満たすためのものです。キャッシュの変化などによる変動を減らすために、各テストは2回続けて実行され、2回目の実行のタイミングが取られました。
array size Array.Clear Eric J. Panos Theof Petar Petrov Darth Gizka
-------------------------------------------------------------------------------
1000: 0,7 µs 0,2 µs 0,2 µs 6,8 µs 0,2 µs
10000: 8,0 µs 1,4 µs 1,2 µs 7,8 µs 0,9 µs
100000: 72,4 µs 12,4 µs 8,2 µs 33,6 µs 7,5 µs
1000000: 652,9 µs 135,8 µs 101,6 µs 197,7 µs 71,6 µs
10000000: 7182,6 µs 4174,9 µs 5193,3 µs 3691,5 µs 1658,1 µs
100000000: 67142,3 µs 44853,3 µs 51372,5 µs 35195,5 µs 16585,1 µs
このコードのパフォーマンスが十分でない場合、有望な手段は、線形コピーループ(すべてのスレッドが同じソースブロックを使用)を並列化するか、古き良き友人P / Invokeです。
注:ブロックのクリアと入力は通常、MMX / SSE命令などを使用して高度に専門化されたコードに分岐するランタイムルーチンによって行われるため、適切な環境では、それぞれの道徳的同等物を呼び出すだけstd::memset
で、専門的なパフォーマンスレベルが保証されます。IOW、権利により、ライブラリー関数Array.Clear
はすべての手巻きバージョンをそのままにしておきます。それが逆になっているという事実は、物事が実際にどれだけ離れているかを示しています。同じFill<>
ことは、そもそも自分自身をロールバックする必要がある場合にも当てはまります。なぜなら、それはまだコアとスタンダードだけにあり、フレームワークにはないからです。.NETは今から約20年前から存在しており、最も基本的なものについてはP / Invokeを左右に実行するか、独自にロールバックする必要があります...