Является ли memset () более эффективным, чем цикл for в C?

memset() более эффективен, чем for цикл.

Учитывая этот код:

char x[500];
memset(x,0,sizeof(x));

И этот:

char x[500];
for(int i = 0 ; i < 500 ; i ++) x[i] = 0;

Какой из них эффективнее и почему? Есть ли какие-либо специальные инструкции в оборудовании для инициализации на уровне блоков.


person David    schedule 09.09.2011    source источник
comment
да. Нет. Может быть. По-разному. Единственный способ получить полезный ответ - это проанализировать и профилировать его в вашей среде. Какой из них быстрее на моем компиляторе, в моей программе, на моем компьютере, ничего полезного вам не скажет.   -  person Robᵩ    schedule 10.09.2011
comment
Скомпилируйте оба и сравните их. Ответ зависит от того, какой компьютер вы используете, какой компилятор вы используете, какую стандартную библиотеку вы используете, размер блока, который вы пытаетесь изменить, фазу луны ...   -  person Chris Lutz    schedule 10.09.2011
comment
@Chris: Скорее всего, тебе даже не нужно заходить так далеко. Достаточно просто взглянуть на результат сборки.   -  person Ed S.    schedule 10.09.2011
comment
Зачем беспокоиться о расследовании? Если нет данных, свидетельствующих об обратном (вы не достигли поставленных целей по производительности и не указываете на этот раздел кода), этот фрагмент кода, скорее всего, не является «горячей точкой», и вам следует просто использовать как можно более простой, читаемый и поддерживаемый код.   -  person Michael    schedule 10.09.2011
comment
@Ed S. - Не все умеют читать сборку. Все могут читать числа. (Ну, все, кто программирует.)   -  person Chris Lutz    schedule 10.09.2011
comment
Если у вас есть компилятор, который не заменяет этот цикл функцией memset (), вам следует найти другой компилятор.   -  person Hans Passant    schedule 10.09.2011
comment
@Chris: Ммммм .... тогда им, наверное, стоит поучиться. Думаю, я 27-летний динозавр, но у меня проблема с так называемыми инженерами, которые не умеют читать базовую сборку ... Я не имею в виду, что профайлер не следует использовать, но для такого тривиальное сравнение в нем не должно быть необходимости.   -  person Ed S.    schedule 10.09.2011
comment
@Ed S. - Если вы занимаетесь программированием на C, я согласен. Если вы веб-программист, которому на самом деле не нужно работать с чем-то более низким, чем Python, то да. Я никогда не скажу, что вам не нужно чему-то учиться, но в некоторых контекстах знание сборки может быть не очень полезным (а в некоторых контекстах знание внутренней работы операционной системы может быть более полезным) .   -  person Chris Lutz    schedule 10.09.2011
comment
@Chris: Вот почему так много веб-ребят (и девушек), с которыми я столкнулся, пишут приложения, которые работают намного медленнее, чем должны быть. Не обязательно потому, что они не могут читать сборку, но потому, что они никогда не изучили характеристики производительности используемых структур данных и то, как их высокоуровневый код может выполняться, когда он превращается в машинный код. Я отвлекся, это обсуждение для другого места и времени.   -  person Ed S.    schedule 10.09.2011
comment
@Ed S. - Я не согласен с вами - мне бы очень понравилось, если бы Нотч изучил C / сборку / теорию CS / что угодно и заставил Minecraft работать с постоянной скоростью.   -  person Chris Lutz    schedule 10.09.2011
comment
@Chris: Ха-ха, да, пожалуйста, и я тоже :)   -  person Ed S.    schedule 10.09.2011
comment
И если вам нужно сделать это только один раз, сделайте это по определению: char x[500] = {0};, что не повлияет на скорость работы, но сделает код более приятным для меня.   -  person pmg    schedule 06.12.2018


Ответы (7)


Безусловно, memset будет намного быстрее, чем этот цикл. Обратите внимание, как вы обрабатываете один символ за раз, но эти функции настолько оптимизированы, что устанавливают несколько байтов за раз, даже используя, если они доступны, инструкции MMX и SSE.

Я думаю, что типичным примером этих оптимизаций, которые обычно остаются незамеченными, является функция strlen библиотеки C. GNU C. Можно было бы подумать, что он имеет производительность как минимум O (n), но на самом деле он имеет O (n / 4) или O (n / 8) в зависимости от архитектуры (да, я знаю, в большом O () будет то же самое , но на самом деле вы получаете восьмое время). Как? Сложно, но красиво: strlen.

person Diego Sevilla    schedule 09.09.2011
comment
Любой оптимизирующий компилятор заменит цикл for оптимальной последовательностью (которая может быть вызовом memset). - person Stephen Canon; 10.09.2011
comment
Кроме того, не гарантируется, что он будет намного быстрее, даже если компилятор выдает неоптимальный код для цикла. 500 на самом деле не такое большое число, и если произойдет программная или жесткая ошибка страницы, это значительно перевесит стоимость самого цикла. - person Michael; 10.09.2011
comment
@ Стивен Кэнон: Хех. Я компилировал библиотеку C с помощью clang / LLVM, и он заменил цикл memset библиотеки вызовом memset. Ой! Глубокая рекурсия. - person Richard Pennington; 10.09.2011
comment
@ Майкл: об этих ошибках не может быть и речи. Конечно, они могут произойти, но когда выполнение, скажем, 500/8 назначений медленнее, чем выполнение 500 назначений 0? Кроме того, я думаю, что OP использовал 500 в качестве примера. - person Diego Sevilla; 10.09.2011
comment
@Diego это не вопрос, что 500/8 присваивает медленнее или быстрее, чем 500 присваивает 0. Подобные микротесты редко бывают полезными из-за других эффектов в системе. На современном процессоре разница только между сравнениями, вероятно, будет порядка 62 циклов против 500 циклов. Я предполагаю, что если вы столкнетесь с аппаратной ошибкой страницы порядка 10 миллионов циклов во время выполнения кода, то сохраненные вами 438 циклов будут просто шумом. - person Michael; 10.09.2011
comment
Кроме того, если этот фрагмент кода не выполняется сотни тысяч раз в секунду, разница в 438 циклов не будет заметна пользователю, и вы тратите свое время на оптимизацию не связанных с проблемами. - person Michael; 10.09.2011
comment
@ Майкл: тогда зачем микрооптимизировать? :) - person Diego Sevilla; 10.09.2011
comment
@Diego: Похоже, мы согласны :) Микрооптимизации лучше оставить компилятору, который может применить их оптом ко всей вашей кодовой базе. Ручная микрооптимизация уместна, когда вы определили, что часть кода сильно влияет на вашу производительность, и вы не можете определить какие-либо алгоритмические улучшения. Я предлагаю придерживаться memset, поскольку это одна строка кода вместо четырех, что, на мой взгляд, более читабельно. - person Michael; 10.09.2011
comment
@Michael: Я согласен с вами в некоторых моментах ... Во всяком случае, я пытался быть саркастичным ... :) Я согласен в одной строке против четырех. Кроме того, я бы сказал, используйте его, потому что это то, для чего он нужен. Но микрооптимизации важны даже на этом уровне. Да, здесь у нас 500 элементов, и функция, которая, возможно, в этой программе вызывается один раз. Но что, если впоследствии эта функция используется как часть других вычислений и вызывается миллион раз? Тогда эти 400 циклов составляют 400 секунд ... - person Diego Sevilla; 10.09.2011
comment
@ Ричард Пеннингтон: -fno-builtin-memset. - person Stephen Canon; 10.09.2011
comment
Это неверно. O - худшая производительность, и это O (n) - считайте, что строка длины 1 ... Ω - лучшая производительность, и это Ω (n / 8). Это для ясности. :) - person Velda; 23.10.2015
comment
Иногда O () используется вместо o () (строчная сигма). Но все мы понимаем, что разница между ними заключается в константах, которые не учитываются в большом O. - person Diego Sevilla; 24.10.2015
comment
Возможно ли, чтобы несколько процессоров разделяли набор памяти между разными процессорами ... любая идея? - person vicky; 05.11.2015
comment
Настоящее узкое место - это доступ к памяти ... Так что несколько процессоров не сделают это быстрее. Увы, их нужно согласовывать, а копирование памяти - несколько более быстрая операция по сравнению с координацией. - person Diego Sevilla; 05.11.2015
comment
Ссылка не работает. Что вы пытаетесь проиллюстрировать? - person SOFe; 16.02.2019

Что ж, почему бы нам не взглянуть на сгенерированный ассемблерный код, полная оптимизация под VS 2010.

char x[500];
char y[500];
int i;      

memset(x, 0, sizeof(x) );   
  003A1014  push        1F4h  
  003A1019  lea         eax,[ebp-1F8h]  
  003A101F  push        0  
  003A1021  push        eax  
  003A1022  call        memset (3A1844h)  

И твоя петля ...

char x[500];
char y[500];
int i;    

for( i = 0; i < 500; ++i )
{
    x[i] = 0;

      00E81014  push        1F4h  
      00E81019  lea         eax,[ebp-1F8h]  
      00E8101F  push        0  
      00E81021  push        eax  
      00E81022  call        memset (0E81844h)  

      /* note that this is *replacing* the loop, 
         not being called once for each iteration. */
}

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

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

Тем не менее, это зависит от того, что компилятор делает за вас. Просмотр разборки - всегда хороший способ узнать, что именно происходит.

person Ed S.    schedule 09.09.2011
comment
Интересно, что моя версия не привела к преобразованию цикла в memset, но это, вероятно, связано с тем, что для моего теста цикл работал с глобальным значением (в противном случае весь цикл был удален как ненужный). - person Michael; 10.09.2011
comment
@Michael: Я добавил пару вызовов printf, используя x и y, чтобы убедиться, что они не были полностью оптимизированы, поскольку они не используются. Это, конечно, в некоторой степени зависит от компилятора и платформы, но любой наполовину достойный оптимизирующий компилятор должен избавиться от цикла с включенной оптимизацией. - person Ed S.; 10.09.2011
comment
даже memset () и инициализация массива (пример: a [n] = {0}) требует того же кода, что и выглядит. Преимущество memset в том, что размер массива может быть переменной, что невозможно при инициализации. Я прав? - person Rajesh; 21.06.2020
comment
Как осматриваете в разборке. - person young_souvlaki; 22.10.2020
comment
@young_souvlaki: В VS? docs. microsoft.com/en-us/visualstudio/debugger/ - person Ed S.; 13.11.2020

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

Почти для всех компиляторов, которые стоит использовать, memset является внутренней функцией, и компилятор сгенерирует для нее оптимизированный встроенный код.

Другие предлагали профилировать и сравнивать, но я бы не стал беспокоиться. Просто используйте memset. Код прост и понятен. Не беспокойтесь об этом, пока ваши тесты не покажут вам, что эта часть кода является горячей точкой производительности.

person Michael    schedule 09.09.2011

Ответ - «это зависит от обстоятельств». memset МОЖЕТ быть более эффективным, или он может использовать внутри цикл for. Я не могу придумать случая, когда memset будет менее эффективным. В этом случае он может превратиться в более эффективный цикл for: ваш цикл повторяется 500 раз, каждый раз устанавливая значение массива в байтах на 0. На 64-битной машине вы могли бы выполнить цикл, задавая 8 байтов (длинный длинный) за раз, что было бы почти в 8 раз быстрее, и обрабатывать оставшиеся 4 байта (500% 8) в конце.

РЕДАКТИРОВАТЬ:

Фактически, это то, что memset делает в glibc:

http://repo.or.cz/w/glibc.git/blob/HEAD:/string/memset.c

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

http://repo.or.cz/w/glibc.git/blob/HEAD:/sysdeps/x86_64/memset.S

person Bobby Powers    schedule 09.09.2011
comment
Я могу представить ситуацию, когда memset будет менее эффективным: компилятор, который не может встроить (ну). - person orlp; 10.09.2011
comment
Я полагаю, что если бы во время компиляции были известны оба вторых аргумента, большинству людей было бы трудно сопоставить с кодом, сгенерированным компилятором. - person Chris Lutz; 10.09.2011

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

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

person Stephen Canon    schedule 09.09.2011
comment
Вы можете процитировать? - person Juno Woods; 10.12.2015
comment
Попробуйте и убедитесь в этом с помощью любого хорошего оптимизирующего компилятора (например, goo.gl/2mWsxq). Я не уверен, что здесь цитировать. - person Stephen Canon; 10.12.2015
comment
Академические цитаты всегда важны, даже если это серая литература. - person Patrick; 18.01.2020

Согласитесь с вышеизложенным. По-разному. Но наверняка memset быстрее или равен циклу for. Если вы не уверены в своей среде или вам лень тестировать, выберите безопасный путь и используйте memset.

person beetree    schedule 09.09.2011
comment
Пожалуйста, укажите, какой из них вы имеете в виду выше. - person Juno Woods; 10.12.2015

Также можно использовать другие методы, такие как разворачивание цикла, которые уменьшают количество циклов. Код memset () может имитировать знаменитое устройство Даффа < / а>:

void *duff_memset(char *to, int c, size_t count)
{
    size_t n;
    char *p = to;
    n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *p++ = c;
    case 7:      *p++ = c;
    case 6:      *p++ = c;
    case 5:      *p++ = c;
    case 4:      *p++ = c;
    case 3:      *p++ = c;
    case 2:      *p++ = c;
    case 1:      *p++ = c;
            } while (--n > 0);
    }
    return to;
}

Эти уловки раньше использовались для увеличения скорости выполнения. Но на современных архитектурах это имеет тенденцию к увеличению размера кода и увеличению количества промахов в кэше.

Таким образом, невозможно сказать, какая реализация будет быстрее, так как это зависит от качества оптимизаций компилятора, способности библиотеки C использовать преимущества специальных аппаратных инструкций, объема данных, с которыми вы работаете, и функций базовая операционная система (управление ошибками страниц, пропуски TLB, копирование при записи).

Например, в glibc реализация memset (), а также различных других функций копирования / установки, таких как bzero () или strcpy () зависят от архитектуры и позволяют использовать различные оптимизированные аппаратные инструкции, такие как SSE или AVX.

person Rachid K.    schedule 11.02.2021