Подтвердить что ты не робот

Более быстрый способ обнулить память, чем с помощью memset?

Я узнал, что memset(ptr, 0, nbytes) работает очень быстро, но есть ли более быстрый способ (по крайней мере, на x86)?

Я предполагаю, что memset использует mov, однако при обнулении памяти большинство компиляторов используют xor, поскольку это быстрее, правильно? edit1: Неправильно, поскольку GregS указал, что работает только с регистрами. О чем я думал?

И я спросил человека, который знал больше об ассемблере, чтобы посмотреть на stdlib, и он сказал мне, что на x86 memset не в полной мере использует 32-битные регистры. Однако в то время я очень устал, поэтому я не совсем уверен, что правильно понял.

edit2: Я снова просмотрел этот вопрос и провел небольшое тестирование. Вот что я тестировал:

    #include <stdio.h>
    #include <malloc.h>
    #include <string.h>
    #include <sys/time.h>

    #define TIME(body) do {                                                     \
        struct timeval t1, t2; double elapsed;                                  \
        gettimeofday(&t1, NULL);                                                \
        body                                                                    \
        gettimeofday(&t2, NULL);                                                \
        elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; \
        printf("%s\n --- %f ---\n", #body, elapsed); } while(0)                 \


    #define SIZE 0x1000000

    void zero_1(void* buff, size_t size)
    {
        size_t i;
        char* foo = buff;
        for (i = 0; i < size; i++)
            foo[i] = 0;

    }

    /* I foolishly assume size_t has register width */
    void zero_sizet(void* buff, size_t size)
    {
        size_t i;
        char* bar;
        size_t* foo = buff;
        for (i = 0; i < size / sizeof(size_t); i++)
            foo[i] = 0;

        // fixes bug pointed out by tristopia
        bar = (char*)buff + size - size % sizeof(size_t);
        for (i = 0; i < size % sizeof(size_t); i++)
            bar[i] = 0;
    }

    int main()
    {
        char* buffer = malloc(SIZE);
        TIME(
            memset(buffer, 0, SIZE);
        );
        TIME(
            zero_1(buffer, SIZE);
        );
        TIME(
            zero_sizet(buffer, SIZE);
        );
        return 0;
    }

результаты:

zero_1 является самым медленным, за исключением -O3. zero_sizet является самым быстрым с примерно равной производительностью по -O1, -O2 и -O3. memset всегда был медленнее, чем zero_sizet. (в два раза медленнее для -O3). одна вещь интересна тем, что при -O3 zero_1 был столь же быстрым, как и zero_sizet. однако дизассемблированная функция имела примерно в четыре раза больше инструкций (я думаю, что это вызвано разворачиванием цикла). Кроме того, я попытался оптимизировать zero_sizet дальше, но компилятор всегда меня превзошел, но здесь нет ничего удивительного.

Теперь, когда побеждает memset, предыдущие результаты были искажены кешем CPU. (все тесты выполнялись на Linux) Дальнейшее тестирование необходимо. Я попробую следующий ассемблер:)

edit3: исправлена ​​ошибка в тестовом коде, результаты теста не затронуты

edit4: Во время разворачивания демонтированной среды VS2010 C я заметил, что memset имеет оптимизированную для SSE процедуру для нуля. Это будет трудно превзойти.

4b9b3361

Ответ 1

x86 - довольно широкий диапазон устройств.

Для полностью общей цели x86 блок сборки с "rep movsd" может взорвать нули в 32-битную память в момент времени. Попытайтесь убедиться, что основная часть этой работы выровнена по DWORD.

Для чипов с mmx петля сборки с movq может достигать 64 бит за раз.

Возможно, вы сможете получить компилятор C/С++ для использования 64-разрядной записи с указателем на длинный или _m64. Цель должна быть выровнена на 8 байт для лучшей производительности.

для чипов с sse, movaps выполняется быстро, но только если адрес равен 16 байтам, поэтому используйте movsb до выравнивания, а затем заполните clear с помощью цикла movaps

Win32 имеет "ZeroMemory()", но я забываю, если это макрос для memset или фактическая "хорошая" реализация.

Ответ 2

memset, как правило, предназначен для очень быстрого кодирования/обнуления кода общего назначения. Он обрабатывает все случаи с разными размерами и выравниваниями, которые влияют на виды инструкций, которые вы можете использовать для выполнения своей работы. В зависимости от того, в какой системе вы находитесь (и от какого поставщика приходит ваш stdlib), базовая реализация может быть в ассемблере, специфичном для этой архитектуры, чтобы использовать все свои собственные свойства. Он также может иметь внутренние специальные случаи для обработки случая обнуления (по сравнению с установкой другого значения).

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

Ответ 3

В настоящее время ваш компилятор должен выполнить всю работу за вас. По крайней мере, из того, что я знаю, gcc очень эффективен в оптимизации вызовов memset (лучше проверить ассемблер, хотя).

Затем также избегайте memset, если вам не нужно:

  • использовать calloc для памяти кучи
  • используйте правильную инициализацию (... = { 0 }) для памяти стека

И для действительно больших кусков используйте mmap, если он у вас есть. Это просто получает нулевую инициализированную память из системы "бесплатно".

Ответ 4

Если я правильно помню (пару лет назад), один из старших разработчиков говорил о быстром пути к bzero() на PowerPC (спецификации сказали, что нам нужно обнулить почти всю память при включении питания). Это может плохо перевести (если вообще) на x86, но это может стоить изучить.

Идея состояла в том, чтобы загрузить строку кэша данных, очистить эту строку кэша данных и затем записать строку очищенного кэша данных обратно в память.

Для чего это стоит, я надеюсь, что это поможет.

Ответ 5

Если у вас нет конкретных потребностей или вы знаете, что ваш компилятор /stdlib сосут, придерживайтесь memset. Он универсальный, и должен иметь достойную производительность в целом. Кроме того, компиляторы могут иметь более легкую оптимизацию времени /inlining memset(), потому что она может иметь внутреннюю поддержку для нее.

Например, Visual С++ часто генерирует встроенные версии memcpy/memset, которые меньше, чем вызов, в библиотечную функцию, что позволяет избежать накладных расходов push/call/ret. И еще возможны оптимизации, когда параметр размера может быть оценен во время компиляции.

Тем не менее, если у вас есть особые потребности (где размер всегда будет крошечным * или * огромным), вы можете получить повышение скорости, опустившись до уровня сборки. Например, использование сквозных операций для обнуления огромных блоков памяти без загрязнения вашего кэша L2.

Но все зависит - и для нормальных вещей, пожалуйста, придерживайтесь memset/memcpy:)

Ответ 7

Функция memset предназначена для гибкости и простоты, даже за счет скорости. Во многих реализациях это простой цикл while, который копирует указанное значение по одному байту за определенное количество байтов. Если вам нужна более быстрая memset (или memcpy, memmove и т.д.), Вы всегда можете скопировать код самостоятельно.

Простейшей настройкой будет выполнение однобайтовых "заданных" операций до тех пор, пока адрес назначения не будет выровнен по 32 или 64 битам (независимо от вашей архитектуры микросхемы), а затем начнет копировать полный регистр ЦП за один раз. Возможно, вам придется выполнить пару однобайтовых "заданных" операций в конце, если ваш диапазон не заканчивается по выровненному адресу.

В зависимости от вашего конкретного процессора у вас также могут быть некоторые потоковые инструкции SIMD, которые могут вам помочь. Как правило, они работают лучше на выровненных адресах, поэтому приведенная выше методика использования выровненных адресов может быть здесь полезной.

Для обнуления больших секций памяти вы также можете увидеть ускорение скорости, разделив диапазон на секции и параллельно обрабатывая каждый раздел (где количество разделов совпадает с числом или ядрами/аппаратными потоками).

Самое главное, нет никакого способа сказать, поможет ли какое-либо из этих вариантов, если вы не попробуете его. Как минимум, взгляните на то, что ваш компилятор испускает для каждого случая. Посмотрите, что другие компиляторы испускают для своей стандартной "memset" (их реализация может быть более эффективной, чем ваш компилятор).

Ответ 8

Существует один фатальный недостаток в этом отличном и полезном тесте: Поскольку memset является первой инструкцией, кажется, есть некоторые "накладные расходы памяти" или так, что делает ее чрезвычайно медленной. Перенос времени memset на второе место и что-то еще на первое место или просто временное запоминающее устройство дважды делает memset самым быстрым со всеми компиляционными переключателями!!!

Ответ 9

Это интересный вопрос. Я сделал эту реализацию, которая немного быстрее (но вряд ли измерима) при компиляции 32-разрядных версий на VС++ 2012. Вероятно, ее можно улучшить на много. Добавление этого в свой класс в многопоточной среде, вероятно, даст вам еще больший прирост производительности, поскольку в многопоточных сценариях есть некоторые проблемы с узким местом с memset().

// MemsetSpeedTest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "Windows.h"
#include <time.h>

#pragma comment(lib, "Winmm.lib") 
using namespace std;

/** a signed 64-bit integer value type */
#define _INT64 __int64

/** a signed 32-bit integer value type */
#define _INT32 __int32

/** a signed 16-bit integer value type */
#define _INT16 __int16

/** a signed 8-bit integer value type */
#define _INT8 __int8

/** an unsigned 64-bit integer value type */
#define _UINT64 unsigned _INT64

/** an unsigned 32-bit integer value type */
#define _UINT32 unsigned _INT32

/** an unsigned 16-bit integer value type */
#define _UINT16 unsigned _INT16

/** an unsigned 8-bit integer value type */
#define _UINT8 unsigned _INT8

/** maximum allo

wed value in an unsigned 64-bit integer value type */
    #define _UINT64_MAX 18446744073709551615ULL

#ifdef _WIN32

/** Use to init the clock */
#define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency);

/** Use to start the performance timer */
#define TIMER_START QueryPerformanceCounter(&t1);

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl;
#else
/** Use to init the clock */
#define TIMER_INIT clock_t start;double diff;

/** Use to start the performance timer */
#define TIMER_START start=clock();

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl;
#endif    


void *MemSet(void *dest, _UINT8 c, size_t count)
{
    size_t blockIdx;
    size_t blocks = count >> 3;
    size_t bytesLeft = count - (blocks << 3);
    _UINT64 cUll = 
        c 
        | (((_UINT64)c) << 8 )
        | (((_UINT64)c) << 16 )
        | (((_UINT64)c) << 24 )
        | (((_UINT64)c) << 32 )
        | (((_UINT64)c) << 40 )
        | (((_UINT64)c) << 48 )
        | (((_UINT64)c) << 56 );

    _UINT64 *destPtr8 = (_UINT64*)dest;
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 2;
    bytesLeft = bytesLeft - (blocks << 2);

    _UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 1;
    bytesLeft = bytesLeft - (blocks << 1);

    _UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll;

    if (!bytesLeft) return dest;

    _UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx];
    for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll;

    return dest;
}

int _tmain(int argc, _TCHAR* argv[])
{
    TIMER_INIT

    const size_t n = 10000000;
    const _UINT64 m = _UINT64_MAX;
    const _UINT64 o = 1;
    char test[n];
    {
        cout << "memset()" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                memset((void*)test, 0, n);  

        TIMER_STOP;
    }
    {
        cout << "MemSet() took:" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                MemSet((void*)test, 0, n);

        TIMER_STOP;
    }

    cout << "Done" << endl;
    int wait;
    cin >> wait;
    return 0;
}

Вывод выполняется следующим образом при компиляции выпуска для 32-разрядных систем:

memset() took:
5.569000
MemSet() took:
5.544000
Done

Вывод выглядит следующим образом при компиляции выпуска для 64-битных систем:

memset() took:
2.781000
MemSet() took:
2.765000
Done

Здесь вы можете найти исходный код Berkley memset(), который я считаю наиболее распространенной версией.

Ответ 10

memset может быть встроен компилятором как ряд эффективных кодов операций, развернутых в течение нескольких циклов. Для очень больших блоков памяти, таких как 4000x2000 64-битный кадровый буфер, вы можете попробовать оптимизировать его в нескольких потоках (которые вы готовите для этой единственной задачи), каждый из которых настраивает свою собственную часть. Обратите внимание, что есть также bzero(), но он более неясен и менее вероятно будет оптимизирован так же, как memset, и компилятор наверняка заметит, что вы передаете 0.

Обычно компилятор предполагает, что вы устанавливаете большие блоки, поэтому для небольших блоков было бы более эффективно просто выполнить *(uint64_t*)p = 0, если вы инициируете большое количество небольших объектов.

Как правило, все процессоры x86 отличаются (если вы не компилируете для какой-либо стандартизированной платформы), и то, что вы оптимизируете для Pentium 2, будет работать по-разному в Core Duo или i486. Поэтому, если вы действительно в это верите и хотите выжать последние несколько кусочков зубной пасты, имеет смысл поставить несколько версий вашего exe, скомпилированных и оптимизированных для разных популярных моделей процессоров. Из личного опыта Clang -march = native увеличил мой игровой FPS с 60 до 65, по сравнению с нет -march.