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



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



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

565   7  

7 ответов:

memset (от <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 (от <algorithm>):

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

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

С memset():

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

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

этот вопрос, хотя и довольно старый, нуждается в некоторых ориентирах, поскольку он требует не самого идиоматического способа или способа, который можно написать в наименьшем количестве строк, но быстрый путь. И глупо отвечать на этот вопрос без фактического тестирования. Поэтому я сравнил четыре решения, memset против std:: fill против нуля ответа 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, который сделал исключение мертвого кода на статических массивах результаты были чрезвычайно сопоставимы, поэтому я покажу только динамическую производительность массива. Маркировка времени-ms для 1 миллиона итераций, используя время.функция часов низкой точности h.

clang 3.8 (используя интерфейс 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/arch: 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 (он делает впечатляющее устранение мертвого кода на статических массивах, а затем имеет ужасную производительность для заполнения). Хотя моя реализация значительно быстрее, это может быть только потому, что она признает, что очистка битов имеет гораздо меньше накладных расходов, чем любая другая операция настройки.

реализация Clang заслуживает большего внимания, поскольку она значительно быстрее. Некоторые дополнительные испытания показали, что его memset оказывается на самом деле специализируется на ноль-не ноль memsets за 400 байт массива значительно медленнее (~220ms) и сравним с ССЗ. Тем не менее, ненулевой memsetting с байтовым массивом 800 делает никакой разницы в скорости, поэтому, наверное, в этом случае их функцию memset, имеет худшую производительность, чем моя реализация--специализация только для небольших массивов, и cuttoff составляет примерно 800 байт. Также обратите внимание, что gcc 'fill' и 'ZERO' не оптимизируются для memset (глядя на сгенерированный код), gcc просто генерирует код с идентичными характеристиками производительности.

вывод: memset на самом деле не оптимизирован для этой задачи, а также люди будут притворяться, что это так (в противном случае GCC и msvc и llvm memset будут иметь одинаковую производительность). Если производительность имеет значение, то memset не должен быть окончательным решением, особенно для этих неудобных массивов среднего размера, потому что он не специализируется на очистке битов и не оптимизирован вручную лучше, чем компилятор может сделать сам по себе.

можно использовать 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);

для статического объявления я думаю, что вы могли бы использовать:

T myarray[100] = {0};

для динамического объявления я предлагаю тот же путь: memset

zero(myarray); все, что вам нужно в C++.

просто добавьте это в другой файл:

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

вот функция, которую я использую:

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 способ, чем с помощью memset. Также вы получите ошибку компиляции, если вы используете динамический массив с указанием размера.

Comments

    Ничего не найдено.