Вот еще одна версия для нас, пользователей Framework, от которой отказались Microsoft. Это в 4 раза быстрее Array.Clear
и быстрее, чем решение Panos Theof и параллельное решение Eric J и Petar Petrov. - в два раза быстрее для больших массивов.
Сначала я хочу представить вам предка функции, потому что это облегчает понимание кода. С точки зрения производительности это в значительной степени наравне с кодом 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);
}
Как видите, это основано на повторном удвоении уже инициализированной части. Это просто и эффективно, но оно противоречит современной архитектуре памяти. Отсюда родилась версия, которая использует удвоение только для создания удобного для кэша начального блока, который затем итеративно обрабатывается по целевой области:
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
качестве ограничения для цикла удвоения, чтобы гарантировать, что в последней операции копирования будет достаточно фуража, чтобы покрыть все, что осталось. Это было бы не так для нечетных подсчетов, если count >> 1
бы вместо этого использовались. Для текущей версии это не имеет значения, поскольку линейный цикл копирования устраняет любую слабину.
Размер ячейки массива должен быть передан в качестве параметра, потому что - уму непостижимо - непатентованные средства не могут использоваться, sizeof
если они не используют ограничение ( unmanaged
), которое может или не может стать доступным в будущем. Неправильные оценки не имеют большого значения, но производительность является наилучшей, если значение является точным по следующим причинам:
Недооценка размера элемента может привести к размерам блоков, превышающим половину кэша L1, что увеличивает вероятность того, что исходные данные копии будут удалены из L1, и их придется повторно выбирать из более медленных уровней кэша.
Завышение размера элемента приводит к недостаточному использованию кэша L1 ЦП, что означает, что цикл копирования линейного блока выполняется чаще, чем при оптимальном использовании. Таким образом, возникает больше фиксированных накладных расходов цикла / вызова, чем это строго необходимо.
Вот тест, с которым сравнивается мой код, Array.Clear
и три других решения, упомянутых ранее. Время для заполнения целочисленных массивов ( Int32[]
) заданных размеров. Чтобы уменьшить отклонения, вызванные капризами кеша и т. Д., Каждый тест был выполнен дважды, и время было взято для второго выполнения.
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
и быть уверенным в профессиональных уровнях производительности. Таким образом, по праву библиотечная функция Array.Clear
должна оставлять все наши свернутые вручную версии в пыли. Тот факт, что все наоборот, показывает, насколько далеки от этого дела. То же самое относится и к тому, что нужно катиться самостоятельно Fill<>
, потому что это все еще только в Core и Standard, но не в Framework. .NET существует уже почти двадцать лет, и нам все еще приходится P / Invoke влево и вправо для самых элементарных вещей или прокручивать свои собственные ...