2015-12-19 4 views
2

Я делаю честную битку в исполнении сфокусированного программирования. Как правило, большинство техник, которые я преподавал, и я знаю, относится к сохранению ОЗУ.Эффективные алгоритмы процессора?

Это, как говорится, я в последнее время решения вопроса здесь Memory efficient AI objects for physics game

Где мне сказали:

это, как правило, процессор, который работает от скорости до того память неработоспособна

Мы провели некоторое тестирование, и казалось, что упаковка/распаковка действительно экономит память, но определенно замедляет производительность.

Но, как я уже сказал, большинство типичных «правил» производительности, которые я видел, имеют дело с сохранением ОЗУ.

Одной из основных тем в скорости программы, например, является динамическое распределение памяти, которое также ориентировано на сохранение ОЗУ.

Что я хочу знать: что делает код ЦП эффективным? Имеют ли языки более низкого уровня, такие как C, большую гибкость для эффективности ЦП? Если да, то почему/как?

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

+2

Как правило, все, что вам нужно, это алгоритм с низкой сложностью. Затем компилятор может выполнить некоторую оптимизацию, и вы настроены. – Alexguitar

+0

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

ответ

4

Profiler

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

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

Для меня № 2 на самом деле довольно большой. Я действительно не начал очень многому научиться, пока у меня не было профилировщика. Это то, как вы можете многому научиться программированию, работая над реальным, значительным проектом и просматривая вещи, которые возникают посередине. Точно так же изучение микроэффективности и компьютерной архитектуры, как правило, проще с помощью профилировщика, поскольку вы преследуете одну точку доступа за другой и исследуете, почему она существует.

Оптимизация памяти

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

Оговорка: это будет несколько упрощен и не вдаваться в темы дизайна компилятора как выделение регистров и стека разливы или даже очень подробное описание иерархии памяти.

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

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

[4 kilobyte chunk][*4 kilobyte chunk][4 kilobyte chunk][...] 
// '*' indicates the chunk that's loaded in. 

При запросе доступа к виртуальной памяти в любом месте, окружающей выровненной, 4 килобайта кусок, система будет страница в этом куске к DRAM. Но мы еще не закончили. Как правило, прежде чем мы можем что-либо сделать, мы должны загружать из DRAM в кэш CPU, который сам по себе разделен на иерархию. В тех случаях, память может быть загружена в 64-байтовых выровнены строками кэша фрагмента, например, так:

[64-byte chunk][64-byte chunk][*64-byte chunk][...] 

... поэтому доступ к памяти заканчивает загрузку из DRAM в кэш процессора таким образом. Когда вы запрашиваете доступ к памяти в DRAM вокруг одного из этих 64-байтовых фрагментов, весь 64-байтовый кусок загружается в кеш процессора.

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

[64 bits][64 bits][64 bits][*64 bits][64 bits][...] 

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

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

Оптимизация памяти

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

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

Использование данных Перед Это выселяют

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

Другой случай, когда это отображается, находится в управляемых языках, где каждый пользовательский тип должен быть выделен отдельно (например: через сборщик мусора), но агрегирован в структуру списка на основе массива. В этом случае, хотя мы храним массив этих объектов, каждый объект фактически представлен посредством ссылки (например, указателя), которая указывает где-то еще в памяти.

Это может быть одной из самых веских причин использовать язык, такой как C или C++. Они позволяют агрегировать определенные пользователем типы, а также распределять их по стеку (который имеет большую временную локальность).

TL; DR

Если вы хотите узнать больше об этих предметах, я предлагаю исследование локальности ссылок.Эта статья также обязательна: http://lwn.net/Articles/250967/

И последнее, но не менее важное: если мне разрешен бесстыдный штекер для вопроса о щедрости, на который я потратил много времени, чтобы ответить: What is the most efficient way to represent small values in a struct?.

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

Update

Советы мудрости в хорошем ответе Jenz»также дал мне желание включать отказ от ответственности, в том, что алгоритмическая эффективность все еще имеет тенденцию быть первым и главное, чтобы беспокоиться о. Я работал с теми типами, которые говорят об эффективности кеш-памяти и многопоточности в течение всего дня, имея дело с самыми субоптимальными алгоритмами, и это неэффективная приоритизация. Микрооптимизация или распараллеливание типа пузыря на миллион элементов далеко не эффективны, как вопиющий пример.

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

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

распределители памяти

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

Существуют также структуры данных, такие как развернутые списки, которые часто игнорируются, поскольку они не предлагают никаких алгоритмических преимуществ по связанному списку. Тем не менее они предлагают существенно большие преимущества с точки зрения эффективности памяти, и в тех сценариях, где у нас есть две структуры данных, которые имеют схожую алгоритмическую сложность, но значительно отличающиеся макеты памяти, победитель, как правило, имеет более эффективную структуру памяти и шаблоны доступа. Развернутый список связывает массивы элементов вместе, а не отдельные элементы, и, опять же, пространственная локальность сильно благоприятствует непрерывным представлениям на основе массива.

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

+0

Ну, ваш ответ чрезвычайно подробный и тщательный, и «профайлер» может быть самым полезным ответом, но я не уверен, действительно ли вы действительно задали вопрос. Опять же, я, возможно, недостаточно объяснил это. Я дал вам преимущество, но многое из того, что вы обсуждали, относится к манипуляциям на уровне сборки, о которых я упоминал, выходит за рамки вопроса. Кстати, вы, кажется, все равно ответили на эту тему. – bigcodeszzer

+0

@bigcodeszzer О, это темы компьютерной архитектуры, но вам не нужно писать код сборки, чтобы извлечь выгоду из эффективности памяти. Например, если вы пишете код на Java, вы можете избежать 'ArrayList'' Integer' в пользу массива 'ints', так как' Integer' должен быть выделен индивидуально (рассеивая память и теряя локальность ссылки) в частности, пространственной локальности). Концепции применяются независимо от языка, поскольку аппаратное обеспечение одинаково. –

+0

Правильно, я просто не уверен, как вы действительно можете сделать некоторые из вещей, о которых говорили в «Оптимизация памяти» и «Выселение данных» на таких языках, как C, C++ или Java? – bigcodeszzer

2

Что делает код ЦП эффективным?

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

Имеют ли языки нижнего уровня, такие как C, большую гибкость для эффективности процессора?

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

1

Общий вопрос заслуживает общий ответ:

Все оптимизации является осуществление кэширования.

Особенно на современных многоуровневых архитектурах кеша.

Осторожно глупую мысль, что все, что вам нужно сделать, это напихать код в кэше команд уровня 1, и все ваши данные в кэше данных 1-го уровня, для того, чтобы эффективно вычислить, что O (N) алгоритм , потому что вместе идет гений, который живет и дышит упражнением, делая тяжелую работу с помощью O (1) в большом столе.

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

+0

Тогда есть узкое место фон Неймана ... – g24l

1

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

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

Заманчиво думать, что все приятные черты продвинутых языков бесплатны или, самое большее, незначительны, , но на самом деле они могут умножать друг друга, как this example shows. Вы все еще можете использовать расширенные языки, но если вы знаете, как выполнять настройку производительности, вы можете воспользоваться преимуществами своих дополнительных функций, не платя за то, что вам не нужно.

Смежные вопросы