2012-03-02 5 views
2

Предположим, что у меня есть код C++ со многими небольшими функциями, в каждом из которых мне обычно потребуется матрица float M1 (n, p) с n, p, известная во время выполнения, чтобы содержать результаты промежуточных вычислений (нет необходимости инициализировать M1, просто объявить его, потому что каждая функция будет просто перезаписывать все строки M1).Создание разрушения матрицы в передовой практике C++?

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

Лучше всего создать временную M1 (n, p) внутри каждой функции или, вернее, раз и навсегда в main() и передать ее каждой функции в виде своего рода ведра, который каждая функция может использовать как лом?

n и p часто умеренно большие [10^2-10^4] для n и [5-100] для p.

(изначально размещен в кодексе codexview, но перемещен здесь).

Бест,

+0

зависит. Распределение памяти дорого. начните с локальных распределений и измените, если allocs являются слишком дорогостоящими. – Anycorn

+1

@ Распределение памяти из Aycycorn, вероятно, менее дорогое, чем доступ к 500 или более значениям, и, безусловно, менее дорогостоящий, чем доступ к миллионам значений. –

+0

@JamesKanze +1 относительная эффективность распределения кучи обычно будет тривиальной по сравнению с этими операциями. Я подумываю о редактировании сообщения, чтобы полностью не предлагать маршрут оптимизации. – stinky472

ответ

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

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

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

ПРЕДУПРЕЖДЕНИЕ. Следующее предназначено только в том случае, если вы действительно хотите выжать максимум из каждого цикла. Важно понимать №4, а также получить хороший профилировщик. Также стоит отметить, что вы, вероятно, сделаете лучше, оптимизируя шаблоны доступа к памяти для этих матричных алгоритмов, чем пытаетесь оптимизировать распределение кучи.


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

Другими словами:

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

Вперед и создайте M1 как временное в каждой функции. Старайтесь не требовать от клиента создания какой-либо матрицы, которая не имеет для него значения, только для вычисления промежуточных результатов. Это будет раскрывать детали оптимизации, которые мы должны стремиться не делать при разработке интерфейсов (скрыть все детали, о которых клиенты не должны знать).

Вместо этого сосредоточьтесь на более общих понятиях, если вы абсолютно хотите, чтобы эта опция ускорила создание этих временных рядов, например, дополнительный распределитель. Это согласуется с практическим дизайном, как с std::set:

std::set<int, std::less<int>, MyFastAllocator<int>> s; // <-- okay 

Несмотря на то, что большинство людей просто сделать:

std::set<int> s; 

В вашем случае это может быть просто: M1 my_matrix (п, р, Alloc) ;

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

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


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

+1

Каждый всегда думает, что может побить стандартный распределитель.Вы не думаете, что разработчики std :: allocator могли бы подумать об этом? – CashCow

+0

@CashCow стандартный распределитель очень общий. Например, предполагается, что мы освободим выделенные блоки сразу. Я на самом деле создал простой распределитель пула памяти на основе стека. Он выделяет память в среднем 4 цикла, в отличие от стандартного распределителя, который имеет тенденцию занимать около 400 на наших платформах (общий случай сводится к увеличению одного указателя). Мой особый трюк: я не освобождаю память в освобождении! Обычно это утечка, за исключением того, что на распределителе есть функция очистки, но это делает ее опасной для использования, и мы используем ее только в наиболее критичных для производительности частях нашего raytracer. – stinky472

+1

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

1

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

2

Не используйте преждевременную оптимизацию. Создайте что-то, что работает правильно и хорошо, и оптимизируйте позже, если будет показано, что он медленный.

(Между прочим, я не думаю, что stackoverflow также подходит для него).

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

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

Дизайн вашей матрицы должен быть оптимальным. Мы должны были бы взглянуть на этот дизайн.

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

+0

:> спасибо за совет. В этом случае основная функция должна быть повторена на большом количестве независимых входных матриц X_ {1} ... X_ {k}, поэтому параллелизм выполняется на «более высоком» уровне, чем функция i пишущий, который для одного из этих X_ {k} [надеюсь, это понятно - если не дайте мне знать и я его перефразую]. – user189035

+1

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

+0

Да, я понял вашу точку зрения, и она в целом полностью верна. Я хотел дать некоторую контекстную информацию. – user189035

1

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

Однако, с точки зрения пользователя, это может скоро стать раздражающим.

я часто обнаруживал, что это достаточно просто в C++, чтобы получить лучшее из обоих миров, просто предлагающее оба пути:

int compute(Matrix const& argument, Matrix& buffer); 

inline int compute(Matrix const& argument) { 
    Matrix buffer(argument.width, argument.height); 
    return compute(argument, buffer); 
} 

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

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

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