Состояние функциональности "memset" в C++ с современными компиляторами

Контекст:

Некоторое время назад я наткнулся на статью Александреску в DDJ 2001 года: http://www.ddj.com/cpp/184403799

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

Цитировать:

There is a very deep, and sad, realization underlying all this. We are in 2001, the year of the Spatial Odyssey. (...) Just step out of the box and look at us — after 50 years, we're still not terribly good at filling and copying memory.

Вопрос:

  1. у кого-нибудь есть более свежая информация об этой проблеме? Работают ли последние реализации GCC и Visual C++ значительно лучше, чем 7 лет назад?
  2. Я пишу код, срок службы которого составляет 5+ (возможно, 10+) лет, и который будет обрабатывать массивы размером от нескольких байтов до сотен мегабайт. Не могу предположить, что мой выбор сейчас по-прежнему будет оптимальным через 5 лет. Что я должен делать:
    • а) использовать набор памяти системы (или эквивалент) и забыть об оптимальной производительности или предположить, что среда выполнения и компилятор справятся с этим за меня.
    • б) раз и навсегда выполнить эталонный тест для массивов различных размеров и компиляторов и переключаться во время выполнения между несколькими подпрограммами.
    • c) запускать тест при инициализации программы и переключаться во время выполнения на основе точных (?) данных.

Обновлено: я работаю над программным обеспечением для обработки изображений. Мои элементы массива - это POD, и каждая миллисекунда на счету!

Изменить 2: Спасибо за первые ответы, вот дополнительная информация:

  • Инициализация буфера может составлять 20-40% от общего времени выполнения некоторых алгоритмов.
  • Платформа может измениться в ближайшие 5+ лет, хотя она останется в категории «самые быстрые процессоры, которые можно купить в DELL за деньги». Компиляторы будут представлять собой некую форму GCC и Visual C++. Никаких встроенных вещей или экзотических архитектур на радаре
  • Я хотел бы услышать от людей, которым пришлось обновлять свое программное обеспечение, когда появились MMX и SSE, так как мне придется сделать то же самое, когда станет доступен "SSE2015" ... :)
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
19
0
5 538
12

Ответы 12

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

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

rlerallut 05.10.2008 16:54

В статье DDJ признается, что memset - лучший ответ, и он намного быстрее, чем то, чего он пытался достичь:

There is something sacrosanct about C's memory manipulation functions memset, memcpy, and memcmp. They are likely to be highly optimized by the compiler vendor, to the extent that the compiler might detect calls to these functions and replace them with inline assembler instructions — this is the case with MSVC.

Итак, если у вас работает memset (то есть вы инициализируете одним байтом), используйте его.

Хотя каждая миллисекунда может считаться, вы должны установить, какой процент времени выполнения теряется на установку памяти. Скорее всего, это очень мало (1 или 2% ??), учитывая, что у вас также есть полезная работа. Учитывая, что усилия по оптимизации, вероятно, будут иметь гораздо лучшую доходность в другом месте.

При выполнении некоторых алгоритмов инициализация временного «вспомогательного» массива может составлять от 20% до 40% от общего времени выполнения. И мои данные часто многобайтовые, поэтому memset в этом случае не работает (я уже использую его для однобайтовых данных).

rlerallut 05.10.2008 17:03

Ну, все зависит от вашей проблемной области и ваших спецификаций, сталкивались ли вы с проблемами производительности, не уложились в срок и определили memset как корень всего зла? Если это так, вы находитесь в единственном случае, когда вы могли бы подумать о настройке memset.

Тогда вы также должны иметь в виду, что набор мемов в любом случае будет зависеть от оборудования, на котором он работает, будет ли программное обеспечение работать на той же платформе в течение этих пяти лет? На той же архитектуре? Как только вы придете к такому выводу, вы можете попробовать «свернуть свой собственный» memset, обычно играя с выравниванием буферов, убедившись, что вы сразу обнуляете 32-битные значения в зависимости от того, что наиболее эффективно для вашей архитектуры.

Однажды я столкнулся с тем же для memcmpt, где накладные расходы на выравнивание вызывали некоторые проблемы, но обычно это не приводит к чудесам, только небольшое улучшение, если оно есть. Если вы упускаете свои требования из-за строгости, это не поможет вам продвинуться дальше.

Это зависит от того, что вы делаете. Если у вас очень конкретный случай, вы часто можете значительно превзойти системную libc (и / или встраивание компилятора) memset и memcpy.

Например, для программы, над которой я работаю, я написал выровненный по 16 байт memcpy и memset, предназначенный для небольших объемов данных. Memcpy был создан только для размеров, кратных 16, больше или равных 64 (с данными, выровненными по 16), а memset был создан только для размеров, кратных 128. Эти ограничения позволили мне получить огромную скорость, и, поскольку я контролировал приложение, я мог адаптировать функции именно к тому, что было необходимо, а также настроить приложение для согласования всех необходимых данных.

Скорость memcpy была примерно в 8-9 раз выше, чем у встроенной в Windows скорости memcpy, сокращая 460-байтовую копию до 50 тактовых циклов. Набор памяти был примерно в 2,5 раза быстрее, чрезвычайно быстро заполняя массив стека нулями.

Если вас интересуют эти функции, их можно найти здесь; опуститесь примерно до строки 600 для memcpy и memset. Они довольно банальны. Обратите внимание, что они предназначены для небольших буферов, которые должны находиться в кеше; если вы хотите инициализировать огромные объемы данных в памяти, минуя кеш, ваша проблема может быть более сложной.

Ура программному обеспечению с открытым исходным кодом! Жаль, что это GPL, я не могу отказаться от использования ее непосредственно в моем приложении :). Но большое спасибо за подтверждение того, что я думал о небольших наборах данных: вы можете превзойти систему memset / cpy. Теперь все, что мне нужно, это метод выбора метода, который будет использоваться для малых / средних / больших наборов данных.

rlerallut 05.10.2008 23:57

Обратите внимание, что, хотя вы можете превзойти системный memset / cpy, 32-разрядная версия Windows на много медленнее, чем должна быть, особенно memcpy, которой удается копировать только около одного байта за такт. Я подозреваю, что новые библиотеки Windows лучше, как и Linux (хотя моя все еще превосходит их, хотя и не так сильно).

Dark Shikari 06.10.2008 00:31

Windows не предоставляет memcpy, в отличие от вашего компилятора C.

Billy ONeal 09.07.2010 17:14

Вам следует попробовать написать версию AVX для Sandy Bridge: en.wikipedia.org/wiki/Advanced_Vector_Extensions. Интересно, в какой момент ограничивающим фактором является пропускная способность памяти, а не пропускная способность процессора. Я полагаю, что даже если пропускная способность памяти ограничивает вашу пропускную способность memcpy, использование более эффективных инструкций даст вам больше циклов процессора, чтобы потратить в параллели с memcpy.

Josh Haberman 04.03.2011 20:30

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

Если проблема с памятью, используйте буфер меньшего размера и скопируйте в новый буфер со смещениями sizeof (..).

HTH

Memset / memcpy в основном написаны с учетом базового набора инструкций, и поэтому могут уступать в производительности специализированным подпрограммам SSE, которые, с другой стороны, налагают определенные ограничения выравнивания.

Но чтобы свести его к списку:

  1. Для наборов данных <= несколько сотен килобайт memcpy / memset работает быстрее, чем все, что вы могли бы смоделировать.
  2. Для наборов данных> мегабайт используйте комбинацию memcpy / memset для выравнивания, а затем используйте свои собственные оптимизированные для SSE подпрограммы / откат к оптимизированным подпрограммам от Intel и т. д.
  3. Обеспечьте согласование при запуске и используйте свои собственные SSE-подпрограммы.

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

Здесь - это реализация memcpy от AMD, я не могу найти статью, в которой описывается концепция, лежащая в основе кода.

Это очень информативно, спасибо. Однако меня беспокоят «границы», в которых вы решите использовать тот или иной метод. Они могут (и будут!) Различаться в зависимости от кеша и компилятора.

rlerallut 05.10.2008 23:53

Ты прав. Цифры немного туманные. Но может служить индикатором. Для такого рода проблем не существует золотого правила.

Christopher 06.10.2008 00:24

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

Почему: если вы реализуете свою собственную инициализацию, вы можете получить немного лучшее решение сейчас, но вполне вероятно, что через пару лет время выполнения улучшится. И вы не хотите выполнять ту же работу, что и ребята, обслуживающие среду выполнения.

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

Что ж ... Я склонен согласиться с вами, но "Dark Shikari" опубликовал интересные цифры о том, как он побил memset / cpy для небольших массивов нетривиальными суммами.

rlerallut 05.10.2008 23:59

Вы можете взглянуть на liboil, они (пытаются) предоставить разные реализации одной и той же функции и выбрать самую быструю при инициализации. Liboil имеет довольно либеральную лицензию, так что вы можете использовать ее также для проприетарного программного обеспечения.

http://liboil.freedesktop.org/

Спасибо, выглядит очень интересно. Я обязательно там долго посмотрю. А вы случайно не знаете, собирается ли он непосредственно под MS Visual C++?

rlerallut 06.10.2008 00:05

Извините, я понятия не имею. Может быть, вы можете спросить в списке рассылки, некоторые из последних коммитов выглядят так, как будто они исправили некоторую проблему в Windows.

quinmars 06.10.2008 02:02

У Форум MASM есть много невероятных программистов / любителей на ассемблере, которые до смерти решили эту проблему (посмотрите The Laboratory). Результаты были очень похожи на ответ Кристофера: SSE невероятен для больших, выровненных буферов, но, опустившись, вы в конечном итоге достигнете такого маленького размера, что базовый цикл for будет таким же быстрым.

Не могли бы вы дать глубокую ссылку на конкретную тему форума о memset?

pts 13.03.2014 00:35

Если вам нужно выделить свою память, а также инициализировать ее, я бы:

  • Используйте calloc вместо malloc
  • Измените как можно большую часть моих значений по умолчанию на ноль (например, пусть мое значение перечисления по умолчанию будет равно нулю; или, если значение по умолчанию для логической переменной равно 'true', сохраните обратное значение в структуре)

Причина этого в том, что calloc инициализирует память нулями за вас. Хотя это повлечет за собой накладные расходы на обнуление памяти, большинство компиляторов, вероятно, будут иметь эту процедуру в высокой степени оптимизированной - более оптимизированной, чем malloc / new с вызовом memcpy.

Наверное true for initialization to zero (and yet, a benchmark is needed). And not available for initialization to non-zero values.
rlerallut 06.10.2008 15:40

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

Kevin 06.10.2008 19:22

В некоторых операционных системах для выделения памяти нулю требуется всего одна операция памяти на страницу, поскольку дескрипторы страниц могут иметь «нулевой» флаг.

einpoklum 22.01.2016 20:36

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

Однако это самый быстрый из когда-либо написанных memset:

void memset (void *memory, size_t size, byte value)
{
}

Не делать чего-либо - всегда самый быстрый способ. Можно ли как-нибудь написать алгоритмы, чтобы избежать начального memset? Какие алгоритмы вы используете?

«(...) проблема ограничена факторами, не зависящими от вас (...)» Действительно. Как теперь выбрать лучшее решение? Разумно ориентированным на будущее способом?

rlerallut 06.10.2008 15:39

Если у вас нет хрустального шара, вы не сможете. Единственный вариант для будущей проверки - использовать все, что предоставляет ОС, либо собственные вызовы API, либо динамически подключаемые библиотеки времени выполнения. Таким образом, управление памятью ОС будет оптимизировано для системы, в которой работает ОС (надеюсь).

Skizz 06.10.2008 20:48

Сейчас уже не 2001 год. С тех пор появились новые версии Visual Studio. Я потратил время на изучение мемсета в них. Они будут использовать SSE для memset (если, конечно, есть). Если ваш старый код был правильным, статистически теперь будет быстрее. Но вы можете попасть в неудачный угол. Я ожидаю того же от GCC, хотя код еще не изучал. Это довольно очевидное улучшение и компилятор с открытым исходным кодом. Кто-то создаст патч.

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

rlerallut 06.10.2008 20:04

Другие вопросы по теме