В чем преимущество использования memset () в C - PullRequest
9 голосов
/ 16 декабря 2011

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

Учитывая следующие объявления буфера ...

struct More_Buffer_Info
{
    unsigned char a[10];
    unsigned char b[10];
    unsigned char c[10];
};

struct My_Buffer_Type
{
    struct More_Buffer_Info buffer_info[100];
};

struct My_Buffer_Type my_buffer[5];

unsigned char *p;
p = (unsigned char *)my_buffer;

Помимо наличия меньшего количества строк кода, есть ли преимущество в использовании этого:

memset((void *)p, 0, sizeof(my_buffer));

По сравнению с этим:

for (i = 0; i < sizeof(my_buffer); i++)
{
    *p++ = 0;
}

Ответы [ 6 ]

24 голосов
/ 16 декабря 2011

Это относится как к memset(), так и к memcpy():

  1. Меньше кода: Как вы уже упоминали, оно короче - меньше строк кода.
  2. Более читабельно: Короче, как правило, делает его более читабельным.(memset() более читабелен, чем этот цикл)
  3. Это может быть быстрее: Иногда это может позволить более агрессивную оптимизацию компилятора.(так что это может быть быстрее)
  4. Смещение: В некоторых случаях, когда вы имеете дело с смещенными данными на процессоре, который не поддерживает смещенные обращения, memset() и memcpy() может быть единственным чистым решением.

Чтобы расширить 3-ю точку, компилятор, использующий SIMD и т.п., может сильно оптимизировать memset().Если вместо этого вы пишете цикл, компилятору сначала нужно «выяснить», что он делает, прежде чем попытаться оптимизировать его.

Основная идея здесь заключается в том, что memset() и подобные библиотечные функции в некоторыхсмысл "говорит" компилятору о своих намерениях.


Как упоминалось @Oli в комментариях, есть некоторые недостатки.Я подробно остановлюсь на них здесь:

  1. Вы должны убедиться, что memset() действительно выполняет то, что вы хотите.Стандарт не гласит, что нули для различных типов данных обязательно равны нулю в памяти.
  2. Для ненулевых данных memset() ограничен только 1-байтовым содержимым.Таким образом, вы не можете использовать memset(), если хотите установить для массива int s что-то отличное от нуля (или 0x01010101 или что-то ...).
  3. Хотя редко, есть некоторыеугловые случаи, когда на самом деле можно превзойти компилятор в производительности с помощью собственного цикла. *

* Я приведу один пример этого из моего опыта:

Хотя memset() и memcpy() обычно являются встроенными функциями компилятора со специальной обработкой компилятором, они все еще являются универсальными функциями.Они ничего не говорят о типе данных, включая выравнивание данных.

Таким образом, в некоторых (хотя и редко) случаях компилятор не может определить выравнивание области памяти и, следовательно, должен генерировать дополнительный кодсправиться со смещением.Принимая во внимание, что, если вы программист, на 100% уверены в выравнивании, использование цикла может быть на самом деле быстрее.

Типичным примером является использование встроенных функций SSE / AVX.(например, копирование 16/32-байтового выровненного массива float с). Если компилятор не может определить 16/32-байтовое выравнивание, ему нужно будет использовать неправильно выровненную загрузку / сохранение и / или обработку кода.Если вы просто напишите цикл, используя встроенные функции загрузки / хранения, выровненные по SSE / AVX, вы можете , вероятно, сделать лучше.

float *ptrA = ...  //  some unknown source, guaranteed to be 32-byte aligned
float *ptrB = ...  //  some unknown source, guaranteed to be 32-byte aligned
int length = ...   //  some unknown source, guaranteed to be multiple of 8

//  memcopy() - Compiler can't read comments. It doesn't know the data is 32-byte
//  aligned. So it may generate unnecessary misalignment handling code.
memcpy(ptrA, ptrB, length * sizeof(float));

//  This loop could potentially be faster because it "uses" the fact that
//  the pointers are aligned. The compiler can also further optimize this.
for (int c = 0; c < length; c += 8){
    _mm256_store_ps(ptrA + c, _mm256_load_ps(ptrB + c));
}
7 голосов
/ 16 декабря 2011

Зависит от качества компилятора и библиотек.В большинстве случаев memset лучше.

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

В частности, это может означать использование определенных аппаратных операций для установки больших областей памяти, таких как SSE на x86, AltiVec на PowerPC, NEON на ARM и так далее.Это может быть огромным улучшением производительности.

С другой стороны, используя цикл for, вы говорите компилятору сделать что-то более конкретное: «загрузите этот адрес в регистр. Запишите в него число. Добавьте один в адрес. Запишите числок нему "и так далее.В теории совершенно интеллектуальный компилятор распознал бы этот цикл таким, какой он есть, и все равно превратил бы его в memset;но я никогда не сталкивался с реальным компилятором, который делал бы это.

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

5 голосов
/ 16 декабря 2011

Помните, что это

for (i = 0; i < sizeof(my_buffer); i++)
{
    p[i] = 0;
}

также может быть быстрее, чем

for (i = 0; i < sizeof(my_buffer); i++)
{
    *p++ = 0;
}

Как уже отвечалось, компилятор часто имеет ручные оптимизированные подпрограммы для memset () memcpy () и других строковых функций. И мы говорим значительно быстрее. теперь количество кода, количество инструкций, которые fast memcpy или memset из компилятора, обычно намного больше, чем предложенное вами решение цикла. меньше строк кода, меньше инструкций не означает быстрее.

Во всяком случае, мое сообщение попробуйте оба. разобрать код, увидеть разницу, попытаться понять, задать вопросы при переполнении стека, если вы этого не сделаете. а затем используйте таймер и время для двух решений, вызывайте любую функцию memcpy тысячи или сотни тысяч раз и измеряйте время целиком (чтобы устранить ошибку во времени). Удостоверьтесь, что вы делаете короткие копии, например, 7 или 5 элементов, и большие копии, например, сотни байтов на набор записей, и попробуйте использовать простые числа, пока вы в нем. На некоторых процессорах в некоторых системах ваш цикл может быть быстрее для нескольких элементов, таких как 3 или 5, или что-то в этом роде, очень быстро, хотя и медленно.

Вот один намек на производительность. Память DDR на вашем компьютере, вероятно, имеет ширину 64 бита и должна быть записана по 64 бита за раз, возможно, она имеет ecc, и вам нужно вычислять эти биты и записывать 72 бита за раз. Не всегда это точное число, но следуйте мысли здесь, это будет иметь смысл для 32 бит или 64 или 128 или что-то еще. Если вы выполняете однобайтовую инструкцию записи в оперативную память, аппаратному обеспечению потребуется выполнить одну из двух вещей, если на этом пути нет кэшей, система памяти должна выполнить 64-битное чтение, изменить один байт, а затем напиши это обратно. Без какой-либо аппаратной оптимизации запись 8 байтов в этой одной строке драм-памяти составляет 16 циклов памяти, и драм-память очень и очень медленная, не обманывайте себя числами 1333 МГц.

Теперь, если у вас есть кэш, для первой записи байта потребуется чтение строки кэша из dram, которая представляет собой одну или несколько из этих 64-битных операций чтения, следующие 7 или 15 или любые другие записи байтов, вероятно, будут очень быстро, так как они идут только в кеш, а не в ddr, в конце концов, эта строка кеша становится драмовой, медленной, так что один, два или четыре и т. д. из этих 64-битных или любых других ddr-местоположений. Таким образом, даже если вы только делаете записи, вам все равно нужно прочитать весь этот оперативный диск, а затем записать его, так что в два раза больше циклов, чем нужно. Если возможно, и это с некоторыми процессорами и системами памяти, memset или часть записи memcpy, могут быть одиночными инструкциями с целой строкой кэша или целым местоположением ddr, и нет необходимости в чтении, мгновенная удвоенная скорость. Это не то, как все оптимизации работают, но, надеюсь, даст вам представление о том, как думать о проблеме. Когда ваша программа помещается в кэш в строках кэша, вы можете удвоить или утроить количество выполненных инструкций, если взамен вы получите половину, четверть или более сокращений количества циклов DDR и вы выиграете в целом.

Как минимум, подпрограммы memset и memcpy компилятора будут выполнять байтовую операцию, если начальный адрес нечетный, тогда 16-битный, если не выровнен по 32-битному. Затем 32 бита, если не выровнены на 64 и выше, пока они не достигнут оптимального размера передачи для этого набора команд / системы. На руку они стремятся к 128 битам. Таким образом, наихудшим случаем на переднем конце будет один байт, затем одно половинное слово, затем несколько слов, а затем попадание в основной набор или цикл копирования. В случае передачи ARM 128 битов записано 128 битов на инструкцию. Затем на бэк-энде, если не выровнены, та же самая сделка, несколько слов, одно слово, один байт в худшем случае. Вы также увидите, что библиотеки делают что-то вроде: если число байтов меньше X, где X - небольшое число, например 13 или около того, то оно входит в цикл, подобный вашему, просто скопируйте несколько байтов, потому что количество инструкций и тактов поддерживать этот цикл меньше / быстрее. разберите или найдите исходный код gcc для ARM и, возможно, mips и некоторые другие хорошие процессоры, и посмотрите, о чем я говорю.

3 голосов
/ 16 декабря 2011

Два преимущества:

  1. Версия с memset проще для чтения - это связано с меньшим количеством строк кода, но не так же, как с. Чтобы понять, что делает версия memset, нужно подумать , особенно если вы напишите

    memset(my_buffer, 0, sizeof(my_buffer));
    

    вместо косвенного через p и ненужного приведения к void * (ПРИМЕЧАНИЕ: ненужно, только если вы действительно кодируете на C, а не на C ++ - некоторые люди не понимают разницу ).

  2. memset вероятно , чтобы иметь возможность записывать 4 или 8 байтов за раз и / или использовать специальные инструкции для подсказок кеша; следовательно, это может быть быстрее, чем ваш байтовый цикл. (ПРИМЕЧАНИЕ. Некоторые компиляторы достаточно умны, чтобы распознавать цикл очистки и заменять либо более широкие записи в память, либо вызов memset. Ваш пробег может отличаться. Всегда измеряйте производительность, прежде чем пытаться сбрасывать циклы.)

1 голос
/ 16 декабря 2011

Ваша переменная p требуется только для цикла инициализации. Код для memset должен быть просто

memset( my_buffer, 0, sizeof(my_buffer));

, что проще и менее подвержено ошибкам. Смысл параметра void* в том, что он будет принимать любой тип указателя, явное приведение не требуется, а присвоение указателю другого типа бессмысленно.

Таким образом, одним из преимуществ использования memset() в этом случае является исключение ненужной промежуточной переменной.

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

1 голос
/ 16 декабря 2011

memset предоставляет стандартный способ написания кода, позволяя конкретным библиотекам платформы / компилятора определять наиболее эффективный механизм.В зависимости от размера данных он может, например, делать как можно больше 32-битных или 64-битных хранилищ.

...