Насколько узким местом является выделение / освобождение памяти в типичных реальных программах? Приветствуются ответы от программ любого типа, в которых производительность обычно имеет значение. Достаточно ли приличные реализации malloc / free / garbage collection достаточно быстры, чтобы это было узкое место только в нескольких угловых случаях, или большинство критически важных для производительности программ значительно выиграют от попытки уменьшить количество выделенной памяти или иметь более быстрый malloc / free / реализация сборки мусора?
Примечание: я нет говорю здесь о вещах в реальном времени. Под критичным к производительности я подразумеваю вещи, в которых важна пропускная способность, но не обязательно задержка.
Обновлено: хотя я упоминаю malloc, этот вопрос нет предназначен для C / C++.


Это важно, особенно когда фрагментация растет, и распределителю приходится усерднее искать в больших кучах смежные регионы, которые вы запрашиваете. Большинство приложений, чувствительных к производительности, обычно пишут свои собственные распределители блоков фиксированного размера (например, они запрашивают у ОС память по 16 МБ за раз, а затем распределяют ее фиксированными блоками по 4, 16 КБ и т. д.), Чтобы избежать этой проблемы.
В играх я видел, как вызовы malloc () / free () потребляют до 15% ЦП (в плохо написанных продуктах), а с тщательно написанными и оптимизированными распределителями блоков - всего 5%. Учитывая, что игра должна иметь стабильную пропускную способность в шестьдесят герц, заставлять ее останавливаться на 500 мс, пока иногда запускается сборщик мусора, нецелесообразно.
«Длительная работа» или «Heap-y» не являются хорошими показателями производительности кучи. Как и хорошее использование кешей ЦП, техника есть. Мое финансовое моделирование длилось ~ 8 часов, но объекты были размещены высоко в дереве вызовов, поэтому использовались миллиарды раз, но выделялись один раз. 99% память была из кучи. Раньше Microsoft поддерживала несколько куч (возможно, все еще поддерживает) для одного процесса, поэтому дерево и связанный список могли выделять свои собственные размеры и избегать фрагментации, которая могла бы возникнуть в противном случае. Точно так же помогает сохранение распределения в куче, кратного размеру некоторой базовой единицы. Эти две пушки очень помогают.
Использование стека больше зависит от времени жизни объекта, чем от производительности. Производительность идентична в хорошо построенной программе. Распределение стека действительно упрощает очистку при выходе из области видимости. _alloca () - хороший чит для выделения динамической памяти из стека, но, за исключением простой очистки и, возможно, предотвращения фрагментации, не имеет преимуществ перед malloc (). caligari.dartmouth.edu/doc/ibmcxx/en_US/doc/libref/concepts/…
В общем, затраты на выделение памяти, вероятно, невысоки из-за конфликтов блокировок, алгоритмической сложности или других проблем с производительностью в приложениях наиболее. В общем, я бы сказал, что это, вероятно, не входит в десятку проблем с производительностью, о которых я бы беспокоился.
Теперь захват очень больших блоков памяти может стать проблемой. И я бы беспокоился о том, чтобы захватить память, но не избавиться от нее должным образом.
В языках на основе Java и JVM создание новых объектов происходит очень, очень и очень быстро.
Вот одна достойная статья парня, который знает свое дело, с некоторыми ссылками внизу на другие ссылки по теме: http://www.ibm.com/developerworks/java/library/j-jtp09275.html
Выделение и освобождение памяти с точки зрения производительности - относительно дорогостоящие операции. Вызовы в современных операционных системах должны идти полностью вниз к ядру, чтобы операционная система могла иметь дело с виртуальной памятью, подкачкой / отображением, защитой выполнения и т. д.
С другой стороны, почти все современные языки программирования скрывают эти операции за «распределителями», которые работают с заранее выделенными буферами.
Эта концепция также используется в большинстве приложений, ориентированных на пропускную способность.
Во-первых, поскольку вы сказали malloc, я предполагаю, что вы говорите о C или C++.
Выделение и освобождение памяти обычно является серьезным узким местом для реальных программ. Многое происходит «под капотом», когда вы выделяете или освобождаете память, и все это зависит от системы; память может быть перемещена или дефрагментирована, страницы могут быть реорганизованы - не существует независимого от платформы способа узнать, какой будет эффект. Некоторые системы (например, многие игровые приставки) также не выполняют дефрагментацию памяти, поэтому в этих системах вы начнете получать ошибки нехватки памяти, поскольку память становится фрагментированной.
Типичный обходной путь - заранее выделить как можно больше памяти и удерживать ее, пока ваша программа не завершится. Вы можете использовать эту память для хранения больших монолитных наборов данных или использовать реализацию пула памяти, чтобы распределять ее по частям. Многие реализации стандартной библиотеки C / C++ сами объединяют определенное количество памяти именно по этой причине.
Однако нет двух способов решить эту проблему - если у вас есть чувствительная ко времени программа на C / C++, выполнение большого объема выделения / освобождения памяти убьет производительность.
Как система C или C++ может выполнять дефрагментацию памяти? Для меня дефрагментация будет означать, что указатели, ранее возвращаемые функцией malloc (), устарели и должны быть обновлены. Насколько я знаю, это невозможно на этих языках.
Извините, если я не понял - я имел в виду, что ОС может выполнять дефрагментацию. В операционных системах, использующих разбиение на страницы, память можно перемещать между страницами, а ячейки памяти переназначать на разные страницы.
Почти каждое высокопроизводительное приложение теперь должно использовать потоки для использования параллельных вычислений. Вот тут-то и проявляется настоящий убийца скорости выделения памяти при написании приложений C / C++.
В приложении C или C++ malloc / new должен блокировать глобальную кучу для каждой операции. Даже без состязательных блокировок далеко не бесплатно, и их следует избегать как можно чаще.
Java и C# лучше справляются с этим, потому что потоки были разработаны с самого начала, а распределители памяти работают из пулов для каждого потока. Это можно сделать и на C / C++, но не автоматически.
+1, но верно ли это для C#? Ни в одном описании распределения памяти и сборщика мусора C# я обнаружил уведомление о пулах памяти для каждого потока. Кроме того, они будут дороже, если память будет высвобождаться в другом потоке, чем она выделена.
@peterchen: см. msdn.microsoft.com/en-us/magazine/bb985011.aspx «В многопроцессорной системе поколение 0 управляемой кучи разделено на несколько областей памяти с использованием одной области для каждого потока. Это позволяет нескольким потокам выполнять распределения одновременно, так что монопольный доступ к куче не требуется».
Фактически, современные распределители памяти, такие как tcmalloc и Кладделать, автоматически используют кучи для каждого потока для удовлетворения большинства запросов на выделение. То есть в обычном случае они не используют блокировки. Это делает их быстрыми и масштабируемыми.
@EmeryBerger: Стандартные библиотеки Microsoft и GNU C не очень хорошо себя показали при многопоточном тестировании в 2010 году. Вот почему серьезные проекты, похоже, используют сторонние библиотеки malloc. Возможно, с тех пор библиотеки по умолчанию были улучшены. Я давно не проводил серьезных испытаний.
В Java (и, возможно, в других языках с достойной реализацией GC) размещение объекта очень дешево. В JVM SUN требуется всего 10 циклов ЦП. Malloc в C / C++ намного дороже только потому, что он должен выполнять больше работы.
Тем не менее, даже объекты распределения в Java очень дешевы, выполнение этого для многих пользователей веб-приложения параллельно может привести к проблемам с производительностью, потому что будет запущено больше запусков сборщика мусора. Следовательно, существуют косвенные затраты на выделение памяти в Java, вызванные освобождением памяти, выполняемым сборщиком мусора. Эти затраты сложно определить количественно, поскольку они во многом зависят от вашей настройки (сколько у вас памяти) и вашего приложения.
Если выделение занимает всего 10 циклов, тогда он не может выполнять никакого поиска, он должен быть добавлен в конец выделенной памяти. Обратной стороной является сжатие памяти после сборки мусора для удаления дырок. Если вы делаете много нового / удаляете, это будет плохо работать.
Нет, не будет. JVM выделяет и освобождает большие фрагменты памяти за один раз. Отдельные операции создания / удаления просто требуют и освобождают от этого заранее выделенного пула. Это дешево на очень сильно.
ну причина в том, что SUN JVM (до сих пор) использует копирующий распределитель для нового пространства. есть to и a из космоса, и одна из них всегда полностью пуста.
да, Скиз, вы подчеркнули. Это дешевое распределение сопротивляется уплотнению. А в Java это может снизить производительность, вся сборка мусора, настройка дефрагментации и взлом - большая проблема в java. Вот почему появились новые параметры Collector и новые сборщики для новых Java-машин. А с новыми реализациями мы получаем такие вещи, как StringBuilder, чтобы избавиться от необходимости создавать новые объекты.
Здесь лучше всего работает система распределения памяти c / C++. Стратегия распределения по умолчанию подходит для большинства случаев, но ее можно изменить по своему усмотрению. В системах GC мало что можно сделать для изменения стратегии распределения. Конечно, есть цена, которую нужно заплатить, и это необходимость отслеживать распределения и правильно их освобождать. C++ идет дальше, и стратегию распределения можно указать для каждого класса с помощью оператора new:
class AClass
{
public:
void *operator new (size_t size); // this will be called whenever there's a new AClass
void *operator new [] (size_t size); // this will be called whenever there's a new AClass []
void operator delete (void *memory); // if you define new, you really need to define delete as well
void operator delete [] (void *memory);define delete as well
};
Многие шаблоны STL также позволяют определять собственные распределители.
Как и во всем, что связано с оптимизацией, вы должны сначала определить с помощью анализа времени выполнения, действительно ли выделение памяти является узким местом, прежде чем писать свои собственные распределители.
Это не совсем верно для систем ГХ. У некоторых виртуальных машин Java достаточно параметров конфигурации памяти, чтобы задушить лошадь. Удачи вам в определении, какие из них использовать.
Согласно Технические характеристики MicroQuill SmartHeap, «типичное приложение [...] тратит 40% своего общего времени выполнения на управление памятью». Вы можете принять эту цифру как верхнюю границу, я лично считаю, что типичное приложение тратит около 10-15% времени выполнения на выделение / освобождение памяти. Это редко является узким местом в однопоточном приложении.
В многопоточных приложениях C / C++ стандартные распределители становятся проблемой из-за конфликта блокировок. Здесь вы начинаете искать более масштабируемые решения. Но имейте в виду Закон Амдала.
40%, скорее всего, являются ложным заявлением, чтобы помочь им больше продавать свой продукт. Я предполагаю, что 95% приложений - это 5-20%.
Другие охватывают C / C++, поэтому я просто добавлю немного информации о .NET.
В .NET выделение кучи обычно происходит очень быстро, так как это просто вопрос захвата памяти в нулевой части кучи. Очевидно, это не может продолжаться вечно, и именно здесь на помощь приходит сборка мусора. Сборка мусора может существенно повлиять на производительность вашего приложения, поскольку пользовательские потоки должны быть приостановлены во время сжатия памяти. Чем меньше полных сборов, тем лучше.
Есть несколько способов повлиять на рабочую нагрузку сборщика мусора в .NET. Обычно, если у вас много ссылок на память, сборщику мусора придется выполнять больше работы. Например. за счет реализации графа с использованием матрицы смежности вместо ссылок между узлами сборщику мусора придется анализировать меньше ссылок.
Независимо от того, действительно ли это важно для вашего приложения, зависит от нескольких факторов, и вам следует профилировать приложение с фактическими данными, прежде чем переходить к такой оптимизации.
Практически все вы являетесь базой выключенный, если говорите о куче Microsoft. Синхронизация выполняется легко, как и фрагментация.
Текущая предпочтительная куча - это LFH, (НИЗКИЙФРАГМЕНТАЦИЯ HEAP), она используется по умолчанию в операционных системах Vista + и может быть без особых проблем настроена в XP через gflag.
Легко избежать проблем с блокировкой / блокировкой / конфликтом / пропускной способностью шины, а также множеством проблем с
HEAP_NO_SERIALIZE
вариант во время HeapAlloc или HeapCreate. Это позволит вам создавать / использовать кучу, не входя в блокированное ожидание.
Я бы рекомендовал создать несколько куч с помощью HeapCreate и определить макрос, возможно, mallocx (enum my_heaps_set, size_t);
было бы хорошо, конечно, вам нужно перераспределить, бесплатно также для настройки в соответствии с требованиями. Если вы хотите пофантазировать, сделайте так, чтобы free / realloc автоматически определял, какой дескриптор кучи сам по себе, оценивая адрес указателя или даже добавляя некоторую логику, позволяющую malloc определять, какую кучу использовать на основе идентификатора потока, и создавая иерархия куч по потокам и общих глобальных куч / пулов.
API-интерфейсы Heap * вызываются внутри malloc / new.
Вот хорошая статья о некоторых динамических проблемах управление памятью, с некоторыми даже более хорошими Рекомендации. Для измерения и анализа активности кучи.
LFH меняет скорость выделения на низкую фрагментацию, поэтому мы не можем # ошибаться ...
Я знаю, что отвечал ранее, однако это был ответ на другой ответ, а не на ваш вопрос.
Если я правильно понимаю, вашим критерием использования производительности является пропускная способность, чтобы поговорить с вами напрямую.
Для меня это означает, что вы должны смотреть почти исключительно на NUMAосведомленныйраспределители.
Ни одной из более ранних ссылок; Документ IBM JVM, Microquill C, SUN JVM. Покройте этот момент, поэтому я очень подозреваю их применение сегодня, где, по крайней мере, на AMD ABI, NUMA является выдающимся управляющим процессором памяти.
Руки вниз; реальный мир, фальшивый мир, любой мир ... Технологии запроса / использования памяти с поддержкой NUMA работают быстрее. К сожалению, в настоящее время я использую Windows, и я не нашел "numastat", доступного в Linux.
У моего друг есть написано об этом подробно в его внедрении для ядра FreeBSD.
Несмотря на то, что я могу показать на месте, обычно ОЧЕНЬ большой объем запросов памяти локального узла поверх удаленного узла (подчеркивая очевидное преимущество пропускная способность в производительности), вы можете мрачно протестировать себя, и это, вероятно, именно то, что вам нужно сделать. так как ваша характеристика производительности будет очень специфичной.
Я знаю, что во многих отношениях, по крайней мере, ранняя версия 5.x VMWARE работала довольно плохо, по крайней мере в то время, из-за того, что не использовала NUMA, часто запрашивающие страницы с удаленного узла. Тем не менее, виртуальные машины являются уникальным зверьком, когда речь идет о разделении памяти или контейнеризации.
Одна из ссылок, которые я процитировал, касается внедрения Microsoft API для AMD ABI, который имеет специализированные интерфейсы выделения NUMA для разработчиков пользовательских приложений;)
Вот довольно недавний анализ, визуальный и все такое, от некоторых разработчиков надстроек браузера, которые сравнивают 4 различных внедрения кучи. Естественно, тот, который они развитый, оказывается на первом месте (странно, как люди, которые проводят тестирование, часто показывают самые высокие баллы).
Они действительно охватывают в некоторой степени количественно, по крайней мере, для своего варианта использования, какой точный компромисс между пространством / временем, обычно они определили LFH (о да, и, кстати, LFH - это просто режим, по-видимому, стандартной кучи) или аналогично разработанный подход по существу потребляет значительно больше памяти, однако со временем может потребоваться меньше памяти ... Grafix тоже изящны ...
Однако я думаю, что выбор внедрения HEAP на основе вашей типичной рабочей нагрузки после того, как вы хорошо ее поймете;) - это хорошая идея, но чтобы хорошо понять свои потребности, сначала убедитесь, что ваши основные операции верны, прежде чем оптимизировать эти разногласия;)
Виртуальная машина Java потребует и освободит память из операционной системы практически независимо от того, что делает код приложения. Это позволяет ему захватывать и освобождать память большими кусками, что намного эффективнее, чем выполнение крошечных отдельных операций, как при ручном управлении памятью.
эта статья был написан в 2005 году, и управление памятью в стиле JVM уже было впереди. С тех пор ситуация только улучшилась.
Which language boasts faster raw allocation performance, the Java language, or C/C++? The answer may surprise you -- allocation in modern JVMs is far faster than the best performing malloc implementations. The common code path for new Object() in HotSpot 1.4.2 and later is approximately 10 machine instructions (data provided by Sun; see Resources), whereas the best performing malloc implementations in C require on average between 60 and 100 instructions per call (Detlefs, et. al.; see Resources). And allocation performance is not a trivial component of overall performance -- benchmarks show that many real-world C and C++ programs, such as Perl and Ghostscript, spend 20 to 30 percent of their total execution time in malloc and free -- far more than the allocation and garbage collection overhead of a healthy Java application.
+1 - Я бы даже усилил его: для долго работающих приложений фрагментация является самой большой проблемой распределения.