Ось ще одна версія для нас, користувачів Framework, відмовилася від Microsoft. Це в 4 рази швидше , як Array.Clear
і швидше , ніж рішення Панос Theof в і Ерік Джея і паралельно один Petar Петрова - до двох разів швидше для великих масивів.
Спочатку я хочу представити вам пращура функції, оскільки це полегшує розуміння коду. Виконання продуктивності це майже врівень з кодом 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
замість цього використовувати. Для поточної версії це не має ніякого значення, оскільки цикл лінійної копії дозволить отримати будь-яку слабкість.
Розмір комірки масиву повинен бути переданий як параметр, тому що - mind boggles - generics не дозволяється використовувати, 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
та бути впевненим у професійних рівнях. IOW, за правами, бібліотечна функція Array.Clear
повинна залишати всі наші ручні версії в пилу. Той факт, що це навпаки, свідчить про те, наскільки насправді далекі від удару речі. Це ж стосується того, що потрібно Fill<>
спершу розгорнути власне , оскільки воно все ще є лише в Core та Standard, але не в Framework. .NET існує вже майже двадцять років, і нам залишається P / Invoke ліворуч та праворуч на найосновніші речі або прокручувати власні ...