Скинути масив C int до нуля: найшвидший спосіб?


102

Якщо припустити, що у нас є T myarray[100]T = int, непідписаний int, long long int або unsigned long long int, який найшвидший спосіб скинути весь його вміст до нуля (не тільки для ініціалізації, але для скидання вмісту кілька разів у моїй програмі) ? Може, з мемсетом?

Те саме питання для динамічного масиву, як T *myarray = new T[100].


16
@BoPersson: ну new це C ++ ...
Маттео Італія

@Matteo - ну так. На відповіді не сильно вплинув (до цих пір :-).
Бо Персон

3
@BoPersson: Мені було погано говорити лише про те, memsetколи C ++ якось задіяний ... :)
Matteo Italia

2
У сучасному компіляторі не можна обіграти просту forпетлю. Але, на диво, ви можете зробити набагато гірше, намагаючись бути розумним.
Девід Шварц

Використовуйте структуру і вставте масив всередину неї. Створіть екземпляр, який є усіма нулями. Використовуйте це, щоб нульовувати інших, які ви створюєте. Це добре працює. Ніяких функцій немає, функцій немає, досить швидко.
Xofo

Відповіді:


170

memset(from <string.h>) - це, мабуть, найшвидший стандартний спосіб, оскільки це звичайно рутина, написана безпосередньо в зборі та оптимізована вручну.

memset(myarray, 0, sizeof(myarray)); // for automatically-allocated arrays
memset(myarray, 0, N*sizeof(*myarray)); // for heap-allocated arrays, where N is the number of elements

До речі, в C ++ ідіоматичним способом було б використання std::fill(from <algorithm>):

std::fill(myarray, myarray+N, 0);

які можуть бути оптимізовані автоматично у формат memset; Я впевнений, що він буде працювати так само швидко, як і memsetдля ints, хоча для невеликих типів він може працювати трохи гірше, якщо оптимізатор недостатньо розумний. Все-таки, коли сумніваєтесь, профіль.


10
Що стосується стандарту ISO C 1999 року, насправді не було гарантовано memsetвстановлення цілого числа 0; не було конкретного твердження, що all-bits-zero є представленням 0. Технічна корекція додала таку гарантію, яка включена в стандарт ISO C 2011 року. Я вважаю, що all-bits-zero є дійсним представленням 0для всіх цілих типів у всіх існуючих реалізаціях C і C ++, тому комітет зміг додати цю вимогу. (Немає подібних гарантій для типів з плаваючою комою або вказівниками.)
Кіт Томпсон,

3
Додаючи до коментаря @ KeithThompson: ця гарантія була додана до 6.2.6.2/5 у простому тексті в TC2 (2004); однак якщо бітів для забивання немає, то 6.2.6.2/1 та / 2 вже гарантували, що всі біти-нулі були 0. (За допомогою бітів накладки існує можливість, що all-bits-zero може бути представленням пастки). Але в будь-якому випадку, ТК повинен визнати і замінити несправний текст, тому станом на 2004 рік ми повинні діяти так, ніби C99 завжди містив цей текст.
М.М.

У C, якщо ви правильно розподілили динамічний масив , різниці між двома мемсетами не буде. Правильне динамічне розподіл було б int (*myarray)[N] = malloc(sizeof(*myarray));.
Лундін

@Lundin: звичайно - якщо ти знаєш, коли час компіляції Nє великим , але в переважній більшості випадків, якщо ти використовував, mallocто знав лише під час виконання.
Маттео Італія

@MatteoItalia У нас з'явилися VLAs з 1999 року.
Лундін,

20

Це питання, хоч і досить старе, потребує певних орієнтирів, оскільки воно вимагає не самого ідіоматичного способу, або способу, який можна записати в найменшій кількості рядків, але найшвидшого . І безглуздо відповісти на це питання без фактичного тестування. Тож я порівняв чотири рішення: memset vs. std :: fill vs. ZERO з відповіді AnT та рішення, яке я створив за допомогою інтерактивних даних AVX.

Зауважте, що це рішення не є загальним, воно працює лише на даних 32 або 64 біт. Будь ласка, коментуйте, якщо цей код робить щось неправильне.

#include<immintrin.h>
#define intrin_ZERO(a,n){\
size_t x = 0;\
const size_t inc = 32 / sizeof(*(a));/*size of 256 bit register over size of variable*/\
for (;x < n-inc;x+=inc)\
    _mm256_storeu_ps((float *)((a)+x),_mm256_setzero_ps());\
if(4 == sizeof(*(a))){\
    switch(n-x){\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
    };\
}\
else if(8 == sizeof(*(a))){\
switch(n-x){\
    case 7:\
        (a)[x] = 0;x++;\
    case 6:\
        (a)[x] = 0;x++;\
    case 5:\
        (a)[x] = 0;x++;\
    case 4:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        ((long long *)(a))[x] = 0;break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
};\
}\
}

Я не буду стверджувати, що це найшвидший метод, оскільки я не є експертом з оптимізації низького рівня. Скоріше це приклад правильної реалізації архітектури, яка швидша, ніж memset.

Тепер, про результати. Я обчислював продуктивність для масивів розміром 100 int та довгих довгих масивів, розподілених як статично, так і динамічно, але за винятком msvc, який робив усунення мертвого коду на статичних масивах, результати були надзвичайно порівнянні, тому я покажу лише динамічну продуктивність масиву. Позначення часу - це мс на 1 мільйон ітерацій, використовуючи функцію годинника з низькою точністю time.h.

clang 3.8 (Використовуючи frontend clang-cl, прапорці оптимізації = / OX / arch: AVX / Oi / Ot)

int:
memset:      99
fill:        97
ZERO:        98
intrin_ZERO: 90

long long:
memset:      285
fill:        286
ZERO:        285
intrin_ZERO: 188

gcc 5.1.0 (прапори оптимізації: -O3 -march = native -mtune = native -mavx):

int:
memset:      268
fill:        268
ZERO:        268
intrin_ZERO: 91
long long:
memset:      402
fill:        399
ZERO:        400
intrin_ZERO: 185

msvc 2015 (оптимізаційні прапори: / OX / арка: AVX / Oi / Ot):

int
memset:      196
fill:        613
ZERO:        221
intrin_ZERO: 95
long long:
memset:      273
fill:        559
ZERO:        376
intrin_ZERO: 188

Тут відбувається багато цікавого: llvm вбивство gcc, типові плямисті оптимізації MSVC (це робить вражаюче усунення мертвого коду на статичних масивах, а потім має жахливу продуктивність для заповнення). Хоча моя реалізація значно швидша, це може бути лише тому, що вона визнає, що очищення бітів має значно менші накладні витрати, ніж будь-яка інша операція налаштування.

Реалізація Кланг заслуговує більше уваги, оскільки це значно швидше. Деякі додаткові тестування показують, що його мемсет насправді спеціалізується на нульових - ненульових мемсетах для 400-байтного масиву набагато повільніше (~ 220 мс) і порівнянні з gcc. Однак ненульове запам’ятовування з масивом 800 байтів не має різниці в швидкості, тому, мабуть, тому в такому випадку їх мемсет має більш низьку продуктивність, ніж моя реалізація - спеціалізація призначена лише для невеликих масивів, а обріз - близько 800 байт. Також зауважте, що gcc 'fill' та 'ZERO' не оптимізують мемсет (дивлячись на генерований код), gcc просто генерує код з однаковими характеристиками продуктивності.

Висновок: memset насправді не оптимізований для цього завдання, тому що люди будуть робити вигляд, що це (інакше gcc і msvc та memset llvm матимуть однакову продуктивність). Якщо продуктивність має значення, то memset не повинен бути остаточним рішенням, особливо для цих незручних масивів середнього розміру, оскільки він не спеціалізований для очищення бітів і не оптимізований вручну краще, ніж компілятор може зробити самостійно.


4
Тест без коду та без згадки про версію компілятора та використані параметри? Хм ...
Марк Глісс

У мене вже були версії компілятора (вони були трохи приховані), і я просто додав застосовані застосовні параметри.
Бенджамін

недійсний аргумент типу unary '*' (мають 'size_t {aka unsigned int}') |
Piotr Wasilewicz

Будучи настільки великодушним, щоб написати свій власний оптимізований метод нулінгу - чи не могли б ви пожаліти кілька слів на те, як це працює, і ЧОМУ це швидше? код - це все, але не пояснює себе.
Motti Shneor

1
@MottiShneor Це виглядає складніше, ніж це є. Регістр AVX має розмір 32 байт. Тому він обчислює, скільки значень aвписується в регістр. Потім він перебирає всі 32-байтові блоки, які слід повністю перезаписати, використовуючи вказівну арифметику ( (float *)((a)+x)). Дві внутрішні _mm256символи (починаючи з ) просто створюють нульовий ініціалізований 32-байтовий регістр і зберігають його до поточного вказівника. Це перші 3 рядки. Решта просто обробляє всі особливі випадки, коли останній 32-байтний блок не повинен бути повністю перезаписаний. Це швидше за рахунок векторизації. - Я сподіваюся, що це допомагає.
wychmaster

11

Від memset():

memset(myarray, 0, sizeof(myarray));

Ви можете використовувати, sizeof(myarray)якщо розмір myarrayвідомий під час компіляції. В іншому випадку, якщо ви використовуєте масив динамічного розміру, такий як отриманий через mallocабо new, вам потрібно буде відстежувати довжину.


2
sizeof буде працювати, навіть якщо розмір масиву не відомий під час компіляції. (звичайно, лише тоді, коли це масив)
asaelr

2
@asaelr: У C ++ sizeofзавжди оцінюється під час компіляції (і не може використовуватися з VLA). У C99 це може бути виразом виконання у випадку VLAs.
Ben Voigt

@BenVoigt Ну, питання про те cі c++. Я прокоментував відповідь Алекса, яка говорить "Ви можете використовувати sizeof (myarray), якщо розмір myarray відомий під час компіляції".
asaelr

2
@asaelr: І в C ++ він абсолютно правильний. У вашому коментарі нічого не сказано про C99 або VLA, тому я хотів уточнити це.
Бен Войгт

5

Можна використовувати memset , але лише тому, що наш вибір типів обмежений інтегральними типами.

Загалом у C є сенс реалізувати макрос

#define ZERO_ANY(T, a, n) do{\
   T *a_ = (a);\
   size_t n_ = (n);\
   for (; n_ > 0; --n_, ++a_)\
     *a_ = (T) { 0 };\
} while (0)

Це дасть вам функціонал, схожий на C ++, який дозволить вам "скинути на нулі" масив об'єктів будь-якого типу, не вдаючись до таких хак memset . В основному це аналог C шаблону функції C ++, за винятком того, що вам потрібно чітко вказати аргумент типу.

Крім того, ви можете побудувати "шаблон" для нерозкладених масивів

#define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a))
#define ZERO_ANY_A(T, a) ZERO_ANY(T, (a), ARRAY_SIZE(a))

У вашому прикладі це буде застосовано як

int a[100];

ZERO_ANY(int, a, 100);
// or
ZERO_ANY_A(int, a);

Варто також зазначити, що спеціально для об'єктів скалярних типів можна реалізувати незалежний від макросу тип

#define ZERO(a, n) do{\
   size_t i_ = 0, n_ = (n);\
   for (; i_ < n_; ++i_)\
     (a)[i_] = 0;\
} while (0)

і

#define ZERO_A(a) ZERO((a), ARRAY_SIZE(a))

перетворення вищевказаного прикладу

 int a[100];

 ZERO(a, 100);
 // or
 ZERO_A(a);

1
Я б опустив ;після while(0), тому можна дзвонити ZERO(a,n);, +1 чудова відповідь
0x90

@ 0x90: Так, ви абсолютно праві. Вся суть do{}while(0)ідіоми не потребує ;в макроозначенні. Виправлено.
AnT

3

Для статичного декларування, я думаю, ви можете використовувати:

T myarray[100] = {0};

Для динамічного декларування я пропоную так само: memset


2
Питання говорить: "Не тільки для ініціалізації".
Ben Voigt

2

zero(myarray); це все, що вам потрібно в C ++.

Просто додайте це до заголовка:

template<typename T, size_t SIZE> inline void zero(T(&arr)[SIZE]){
    memset(arr, 0, SIZE*sizeof(T));
}

1
Це неправильно, воно очистить SIZE байтів. 'memset (arr, 0, SIZE * sizeof (T));' було б правильно.
Корнель Киселевич

@KornelKisielewicz D'oh! Я сподіваюся, що ніхто не копіював цю функцію за останні 1,5 року :(
Навін

1
сподіваюся, що ні, я прокоментував, тому що google привів мене сюди :)
Kornel Kisielewicz

1
Зауважте, що ця функція zeroтакож є правильною, наприклад, T=char[10]як це могло б бути у випадку, коли arrаргумент є багатовимірним масивом, наприклад char arr[5][10].
мандрагора

1
Так, я перевірив ряд випадків на gcc 4.7.3. Я вважаю, що це було б добре відзначити для цієї відповіді, оскільки в іншому випадку вам потрібно мати спеціалізацію шаблонів для кожного числа розмірів масиву. Інші відповіді також не узагальнюють, наприклад, ARRAY_SIZEмакрос, який дає неправильний розмір, якщо він використовується у багатовимірному масиві, можливо, краще ім'я ARRAY_DIM<n>_SIZE.
мандрагора

1

Ось функція, яку я використовую:

template<typename T>
static void setValue(T arr[], size_t length, const T& val)
{
    std::fill(arr, arr + length, val);
}

template<typename T, size_t N>
static void setValue(T (&arr)[N], const T& val)
{
    std::fill(arr, arr + N, val);
}

Ви можете назвати це так:

//fixed arrays
int a[10];
setValue(a, 0);

//dynamic arrays
int *d = new int[length];
setValue(d, length, 0);

Вище - це більше C ++ 11 спосіб, ніж використання мемсету. Також ви отримуєте помилку часу компіляції, якщо використовуєте динамічний масив із зазначенням розміру.


оригінальне запитання на C, а не на C ++, отже, std :: fill не може бути правильною відповіддю
Motti Shneor
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.