Предположим a1
, b1
, c1
иd1
точка в динамической памяти , и мой числовой код имеет следующий основной цикл.
const int n = 100000;
for (int j = 0; j < n; j++) {
a1[j] += b1[j];
c1[j] += d1[j];
}
Этот цикл выполняется 10000 раз через другой внешний for
цикл. Чтобы ускорить его, я изменил код на:
for (int j = 0; j < n; j++) {
a1[j] += b1[j];
}
for (int j = 0; j < n; j++) {
c1[j] += d1[j];
}
Скомпилирован на MS Visual C ++ 10.0 с полной оптимизацией и SSE2 включен для 32-битной на Intel Core 2 Duo (x64), первый пример занимает 5,5 секунды, а пример с двойной петлей - всего 1,9 секунды. Мой вопрос: (Пожалуйста, обратитесь к моему перефразированному вопросу внизу)
PS: я не уверен, если это поможет:
Разборка для первого цикла в основном выглядит следующим образом (этот блок повторяется примерно пять раз в полной программе):
movsd xmm0,mmword ptr [edx+18h]
addsd xmm0,mmword ptr [ecx+20h]
movsd mmword ptr [ecx+20h],xmm0
movsd xmm0,mmword ptr [esi+10h]
addsd xmm0,mmword ptr [eax+30h]
movsd mmword ptr [eax+30h],xmm0
movsd xmm0,mmword ptr [edx+20h]
addsd xmm0,mmword ptr [ecx+28h]
movsd mmword ptr [ecx+28h],xmm0
movsd xmm0,mmword ptr [esi+18h]
addsd xmm0,mmword ptr [eax+38h]
Каждый цикл в примере с двойным циклом создает этот код (следующий блок повторяется примерно три раза):
addsd xmm0,mmword ptr [eax+28h]
movsd mmword ptr [eax+28h],xmm0
movsd xmm0,mmword ptr [ecx+20h]
addsd xmm0,mmword ptr [eax+30h]
movsd mmword ptr [eax+30h],xmm0
movsd xmm0,mmword ptr [ecx+28h]
addsd xmm0,mmword ptr [eax+38h]
movsd mmword ptr [eax+38h],xmm0
movsd xmm0,mmword ptr [ecx+30h]
addsd xmm0,mmword ptr [eax+40h]
movsd mmword ptr [eax+40h],xmm0
Вопрос оказался неактуальным, так как поведение сильно зависит от размеров массивов (n) и кэша ЦП. Так что, если есть дальнейший интерес, я перефразирую вопрос:
Не могли бы вы дать некоторое четкое представление о деталях, которые приводят к разным поведениям кэша, как показано пятью областями на следующем графике?
Также было бы интересно указать на различия между архитектурами ЦП и кэш-памяти, предоставляя подобный график для этих ЦП.
PPS: вот полный код. Он использует TBB Tick_Count
для синхронизации с более высоким разрешением, которую можно отключить, не задав TBB_TIMING
макрос:
#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>
//#define TBB_TIMING
#ifdef TBB_TIMING
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif
using namespace std;
//#define preallocate_memory new_cont
enum { new_cont, new_sep };
double *a1, *b1, *c1, *d1;
void allo(int cont, int n)
{
switch(cont) {
case new_cont:
a1 = new double[n*4];
b1 = a1 + n;
c1 = b1 + n;
d1 = c1 + n;
break;
case new_sep:
a1 = new double[n];
b1 = new double[n];
c1 = new double[n];
d1 = new double[n];
break;
}
for (int i = 0; i < n; i++) {
a1[i] = 1.0;
d1[i] = 1.0;
c1[i] = 1.0;
b1[i] = 1.0;
}
}
void ff(int cont)
{
switch(cont){
case new_sep:
delete[] b1;
delete[] c1;
delete[] d1;
case new_cont:
delete[] a1;
}
}
double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
allo(cont,n);
#endif
#ifdef TBB_TIMING
tick_count t0 = tick_count::now();
#else
clock_t start = clock();
#endif
if (loops == 1) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++){
a1[j] += b1[j];
c1[j] += d1[j];
}
}
} else {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
a1[j] += b1[j];
}
for (int j = 0; j < n; j++) {
c1[j] += d1[j];
}
}
}
double ret;
#ifdef TBB_TIMING
tick_count t1 = tick_count::now();
ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
clock_t end = clock();
ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif
#ifndef preallocate_memory
ff(cont);
#endif
return ret;
}
void main()
{
freopen("C:\\test.csv", "w", stdout);
char *s = " ";
string na[2] ={"new_cont", "new_sep"};
cout << "n";
for (int j = 0; j < 2; j++)
for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
cout << s << i << "_loops_" << na[preallocate_memory];
#else
cout << s << i << "_loops_" << na[j];
#endif
cout << endl;
long long nmax = 1000000;
#ifdef preallocate_memory
allo(preallocate_memory, nmax);
#endif
for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
{
const long long m = 10000000/n;
cout << n;
for (int j = 0; j < 2; j++)
for (int i = 1; i <= 2; i++)
cout << s << plain(n, m, j, i);
cout << endl;
}
}
(Показывает FLOP / s для разных значений n
.)
источник
restrict
ключевое слово для таких ситуаций. Я не знаю, есть ли у MSVC нечто подобное. Конечно, если бы это было проблемой, то код SSE был бы неправильным.d1[j]
может совпасть сa1[j]
, так что компилятор может отказаться от некоторой оптимизации памяти. Пока этого не произойдет, если вы разделите записи в памяти в два цикла.Ответы:
После дальнейшего анализа этого, я полагаю, что это (по крайней мере частично) вызвано выравниванием данных четырехочковых. Это приведет к некоторому уровню конфликтов банка / пути кеша.
Если я правильно угадал, как вы размещаете свои массивы, они , скорее всего, будут выровнены по строке страницы. .
Это означает, что все ваши обращения в каждом цикле будут попадать в один и тот же путь кеша. Тем не менее, процессоры Intel некоторое время имели 8-стороннюю ассоциативность L1-кэша. Но на самом деле производительность не совсем одинакова. Доступ к 4-сторонним каналам все еще медленнее, чем, скажем, к 2-сторонним.
РЕДАКТИРОВАТЬ: На самом деле это выглядит так, как будто вы распределяете все массивы отдельно. Обычно, когда запрашиваются такие большие выделения, распределитель запрашивает свежие страницы из ОС. Следовательно, существует высокая вероятность того, что большие выделения появятся при том же смещении от границы страницы.
Вот тестовый код:
Результаты тестов:
РЕДАКТИРОВАТЬ: результаты на фактическом Core 2 архитектуры машины:
2 x Intel Xeon X5482 Harpertown @ 3,2 ГГц:
Замечания:
6,206 секунды с одной петлей и 2,116 секунды с двумя петлями. Это точно воспроизводит результаты ОП.
В первых двух тестах массивы выделяются отдельно. Вы заметите, что все они имеют одинаковое выравнивание по отношению к странице.
Во вторых двух тестах массивы упакованы вместе, чтобы нарушить это выравнивание. Здесь вы заметите, что обе петли быстрее. Кроме того, второй (двойной) цикл теперь стал медленнее, чем вы обычно ожидаете.
Как отмечает @Stephen Cannon в комментариях, очень вероятно, что это выравнивание приведет к ложному наложению псевдонимов в блоках загрузки / хранения или в кеше. Я погуглил по этому поводу и обнаружил, что у Intel на самом деле есть аппаратный счетчик для киосков частичного адреса :
http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html
5 регионов - объяснения
Регион 1:
Это легко. Набор данных настолько мал, что в производительности преобладают издержки, такие как циклы и ветвления.
Регион 2:
Здесь при увеличении размеров данных количество относительных накладных расходов уменьшается, а производительность «насыщается». Здесь два цикла медленнее, потому что он имеет в два раза больше циклов и ветвлений.Я точно не знаю, что здесь происходит ... Выравнивание все еще может сыграть свою роль, так как Agner Fog упоминает о конфликтах банков кеша . (Эта ссылка о Sandy Bridge, но идея должна быть применима и к Core 2.)
Регион 3:
На этом этапе данные больше не помещаются в кэш L1. Таким образом, производительность ограничена пропускной способностью кэша L1 <-> L2.
Регион 4:
Мы наблюдаем падение производительности в одном цикле. И, как уже упоминалось, это связано с выравниванием, которое (наиболее вероятно) вызывает ложные смещения псевдонимов в блоках загрузки / хранения процессора.
Однако для того, чтобы произошло ложное наложение, между наборами данных должен быть достаточно большой шаг. Вот почему вы не видите этого в регионе 3.
Регион 5:
На данный момент ничего не помещается в кэш. Таким образом, вы ограничены пропускной способностью памяти.
источник
ОК, правильный ответ определенно должен что-то делать с кешем процессора. Но использовать аргумент кеша довольно сложно, особенно без данных.
Есть много ответов, которые вызвали много дискуссий, но давайте посмотрим правде в глаза: проблемы с кэшем могут быть очень сложными и не одномерными. Они сильно зависят от размера данных, поэтому мой вопрос был несправедливым: он оказался в очень интересном месте на графике кеша.
Ответ @ Mysticial убедил многих людей (в том числе и меня), вероятно, потому, что он был единственным, кто, казалось, полагался на факты, но это была лишь одна «точка данных» правды.
Вот почему я объединил его тест (используя непрерывное или раздельное распределение) и совет Ответчика @James.
На графиках ниже показано, что большинство ответов и особенно большинство комментариев к вопросу и ответам можно считать совершенно неправильными или верными в зависимости от конкретного сценария и используемых параметров.
Обратите внимание, что мой первоначальный вопрос был при n = 100.000 . Эта точка (случайно) демонстрирует особое поведение:
Он обладает наибольшим расхождением между версией с одним и двумя циклами (почти в три раза)
Это единственная точка, где однопетлевая (а именно с непрерывным распределением) превосходит двухконтурную версию. (Это сделало возможным ответ Mysticial.)
Результат с использованием инициализированных данных:
Результат с использованием неинициализированных данных (это то, что проверил Mysticial):
И это трудно объяснить: инициализированные данные, которые выделяются один раз и используются повторно для каждого следующего контрольного примера с разным размером вектора:
Предложение
Каждый вопрос низкого уровня производительности, связанный с переполнением стека, должен требовать предоставления информации MFLOPS для всего диапазона размеров данных, связанных с кэшем! Это пустая трата времени, чтобы думать об ответах и особенно обсуждать их с другими без этой информации.
источник
n
и он показывает тот же разрыв в производительностиn = 80000, n = 100000, n = 200000
и т. Д.VirtualAlloc
вызов блокируется до тех пор, пока он не станет достаточно обнуленным, чтобы удовлетворить запрос. В отличие от этого, Linux просто отображает нулевую страницу как необходимое для копирования при записи, а при записи копирует новые нули на новую страницу перед записью новых данных. В любом случае, с точки зрения процесса пользовательского режима, страницы обнуляются, но первое использование неинициализированной памяти в Linux обычно обходится дороже, чем в Windows.Второй цикл включает в себя гораздо меньшую активность кеша, поэтому процессору легче справляться с требованиями к памяти.
источник
a[i]
,b[i]
,c[i]
иd[i]
во втором варианте, она должна только два. Это делает гораздо более жизнеспособным пополнение этих строк при добавлении.x += y
) есть два чтения и одна запись. Это верно для любого варианта. Следовательно, требования к пропускной способности кеша <-> CPU одинаковы. До тех пор, пока нет конфликтов, требования к пропускной способности кеша <-> ОЗУ также одинаковы.Представьте, что вы работаете на машине, где
n
было только правильное значение, чтобы можно было одновременно хранить два ваших массива в памяти, но общего объема доступной памяти посредством кэширования на диске все еще было достаточно для хранения всех четырех.Предполагая простую политику кэширования LIFO, этот код:
будет сначала вызывать
a
иb
загружаться в ОЗУ, а затем работать полностью в ОЗУ. Когда начинается второй цикл,c
иd
затем будет загружен с диска в оперативную память и операцию.другой цикл
будет выводить из памяти два массива и страницу в двух других каждый раз вокруг цикла . Это, очевидно, будет намного медленнее.
Вы, вероятно, не видите кеширование диска в своих тестах, но вы, вероятно, видите побочные эффекты какой-либо другой формы кеширования.
Кажется, здесь есть небольшая путаница / недоразумение, поэтому я попытаюсь немного уточнить на примере.
Скажем,
n = 2
и мы работаем с байтами. Таким образом, в моем сценарии у нас всего 4 байта оперативной памяти, а остальная часть памяти значительно медленнее (скажем, в 100 раз больше доступа).Предполагая довольно тупую политику кеширования, если байт не находится в кеше, поместите его туда и получите следующий байт, пока мы на нем, вы получите сценарий примерно такой:
С
кеш,
a[0]
аa[1]
затемb[0]
иb[1]
и установитьa[0] = a[0] + b[0]
в кеш - теперь в кеше четыре байта,a[0], a[1]
иb[0], b[1]
. Стоимость = 100 + 100.a[1] = a[1] + b[1]
в кеш. Стоимость = 1 + 1.c
иd
.Общая стоимость =
(100 + 100 + 1 + 1) * 2 = 404
С
кеш,
a[0]
аa[1]
затемb[0]
иb[1]
и установитьa[0] = a[0] + b[0]
в кеш - теперь в кеше четыре байта,a[0], a[1]
иb[0], b[1]
. Стоимость = 100 + 100.a[0], a[1], b[0], b[1]
из кеша и кеша,c[0]
аc[1]
затемd[0]
иd[1]
и установитьc[0] = c[0] + d[0]
в кеш. Стоимость = 100 + 100.(100 + 100 + 100 + 100) * 2 = 800
Это классический сценарий трэша кеша.
источник
a1
иb1
для первого задания, а не только на первой странице каждой из них? (Вы предполагаете, что 5-байтовые страницы, так что страница составляет половину вашей оперативной памяти? Это не просто масштабирование, это совершенно не похоже на реальный процессор.)Это не из-за другого кода, а из-за кеширования: ОЗУ медленнее, чем регистры ЦП, а кэш-память находится внутри ЦП, чтобы избежать записи в ОЗУ каждый раз, когда переменная изменяется. Но кэш не такой большой, как оперативная память, следовательно, он отображает только ее часть.
Первый код изменяет адреса удаленной памяти, чередуя их в каждом цикле, тем самым постоянно требуя аннулировать кэш.
Второй код не чередуется: он просто передается по соседним адресам дважды. Это заставляет всю работу завершаться в кеше, аннулируя ее только после запуска второго цикла.
источник
Я не могу повторить результаты, обсуждаемые здесь.
Я не знаю, виноват ли плохой тестовый код или что-то в этом роде, но эти два метода находятся в пределах 10% друг от друга на моей машине, используя следующий код, и один цикл обычно немного быстрее двух - как вы ожидать.
Размеры массивов варьировались от 2 ^ 16 до 2 ^ 24 с использованием восьми циклов. Я был осторожен, чтобы инициализировать исходные массивы, чтобы
+=
назначение не просило FPU добавить мусор памяти, интерпретируемый как double.Я играл с различными схемами, такими как сдача задания
b[j]
,d[j]
чтобыInitToZero[j]
внутри петель, а также с использованием+= b[j] = 1
и+= d[j] = 1
, и я получил достаточно стабильные результаты.Как и следовало ожидать, инициализация
b
и использованиеd
внутри циклаInitToZero[j]
дали преимущество комбинированному подходу, так как они выполнялись вплотную перед назначениемa
иc
, но все же в пределах 10%. Иди разберись.Аппаратное обеспечение Dell XPS 8500 с поколением 3 Core i7 процессором @ 3,4 ГГц и 8 ГБ памяти. Для 2 ^ 16 до 2 ^ 24, используя восемь циклов, совокупное время было 44,987 и 40,965 соответственно. Visual C ++ 2010, полностью оптимизирован.
PS: я изменил циклы для обратного отсчета до нуля, и комбинированный метод был немного быстрее. Почесывая голову Обратите внимание на новый размер массива и количество циклов.
Я не уверен, почему было решено, что MFLOPS был релевантным показателем. Хотя идея заключалась в том, чтобы сосредоточиться на доступе к памяти, поэтому я попытался минимизировать время вычислений с плавающей запятой. Я ушел в
+=
, но я не уверен, почему.Прямое назначение без вычислений было бы более точным тестом времени доступа к памяти и создало бы тест, который был бы равномерным независимо от количества циклов. Может быть, я что-то упустил в разговоре, но об этом стоит подумать дважды. Если плюс не входит в задание, то кумулятивное время почти одинаково и составляет 31 секунда.
источник
Это связано с тем, что в процессоре не так много промахов кэша (когда ему приходится ждать получения данных массива из микросхем ОЗУ). Было бы интересно непрерывно корректировать размер массивов так, чтобы вы превышали размеры кэша 1-го уровня (L1), а затем кэша 2-го уровня (L2) вашего ЦП и отображали время, необходимое для вашего кода. выполнить по размерам массивов. График не должен быть прямой линией, как вы ожидаете.
источник
Первый цикл чередует запись в каждой переменной. Второй и третий только делают небольшие скачки размера элемента.
Попробуйте написать две параллельные линии по 20 крестиков с помощью ручки и бумаги, разделенных на 20 см. Попробуйте один раз закончить одну, а затем другую строку и попробуйте другой раз, поочередно написав крестик в каждой строке.
источник
Оригинальный вопрос
Вывод:
Дело 1 - это классическая проблема интерполяции, которая оказывается неэффективной. Я также думаю, что это было одной из основных причин, почему многие машинные архитектуры и разработчики закончили создавать и проектировать многоядерные системы с возможностью выполнения многопоточных приложений, а также параллельного программирования.
Рассматривая его с точки зрения такого подхода, без учета того, как аппаратное обеспечение, ОС и компилятор (-ы) работают вместе для распределения кучи, включающей работу с ОЗУ, кэш-памятью, файлами подкачки и т. Д .; математика, лежащая в основе этих алгоритмов, показывает нам, какой из этих двух вариантов является лучшим решением.
Мы можем использовать аналогию
Boss
существа,Summation
которое будет представлять собойFor Loop
путешествующее между работникамиA
&B
.Мы легко видим, что Случай 2 по меньшей мере вдвое быстрее, если не чуть больше, чем Случай 1, из-за разницы в расстоянии, необходимом для перемещения, и времени, которое требуется рабочим. Эта математика почти виртуально соответствует идеям BenchMark Times, а также количеству различий в инструкциях по сборке.
Теперь я начну объяснять, как все это работает ниже.
Оценивая проблему
Код ОП:
А также
Рассмотрение
Рассматривая оригинальный вопрос ОП о двух вариантах циклов for и его исправленный вопрос о поведении кэшей, а также множество других превосходных ответов и полезных комментариев; Я хотел бы попытаться сделать что-то другое здесь, используя другой подход к этой ситуации и проблеме.
Подход
Учитывая два цикла и все дискуссии о кэшировании и хранении страниц, я бы хотел использовать другой подход, чтобы взглянуть на это с другой точки зрения. Тот, который не использует кеш и файлы подкачки, ни выполнения для выделения памяти, на самом деле, этот подход вообще не касается реального оборудования или программного обеспечения.
Перспектива
Посмотрев некоторое время на код, стало совершенно ясно, в чем проблема и что ее генерирует. Давайте разберем это в алгоритмической задаче и посмотрим на это с точки зрения использования математических обозначений, а затем применим аналогию к математическим задачам, а также к алгоритмам.
Что мы знаем
Мы знаем, что этот цикл будет выполняться 100 000 раз. Мы также знаем , что
a1
,b1
,c1
иd1
являются указателями на 64-битной архитектуре. В C ++ на 32-разрядной машине все указатели имеют размер 4 байта, а на 64-разрядной машине они имеют размер 8 байтов, поскольку указатели имеют фиксированную длину.Мы знаем, что у нас есть 32 байта для выделения в обоих случаях. Единственное отличие состоит в том, что мы выделяем 32 байта или 2 набора по 2-8 байт на каждую итерацию, тогда как во втором случае мы выделяем 16 байтов для каждой итерации для обоих независимых циклов.
Оба цикла по-прежнему равны 32 байта в общем распределении. С этой информацией давайте теперь продолжим и покажем общую математику, алгоритмы и аналогию этих понятий.
Мы знаем, сколько раз один и тот же набор или группа операций должны быть выполнены в обоих случаях. Мы знаем количество памяти, которое нужно выделить в обоих случаях. Мы можем оценить, что общая рабочая нагрузка распределений между обоими случаями будет примерно одинаковой.
Что мы не знаем
Мы не знаем, сколько времени это займет для каждого случая, если только мы не установим счетчик и не проведем тест производительности. Тем не менее, критерии уже были включены из исходного вопроса, а также из некоторых ответов и комментариев; и мы можем видеть существенную разницу между этими двумя понятиями, и в этом вся суть этого предложения по этой проблеме.
Давайте расследовать
Уже очевидно, что многие уже сделали это, взглянув на распределение кучи, тесты производительности, на RAM, Cache и Page Files. Рассмотрение конкретных точек данных и конкретных итерационных индексов также было включено, и различные разговоры об этой конкретной проблеме заставили многих людей начать сомневаться в других связанных с этим вещах. Как мы начинаем смотреть на эту проблему, используя математические алгоритмы и применяя к ней аналогию? Начнем с того, что сделаем пару утверждений! Затем мы строим наш алгоритм оттуда.
Наши утверждения:
F1()
,F2()
,f(a)
,f(b)
,f(c)
иf(d)
.Алгоритмы:
1-й случай: - только одно суммирование, но два независимых вызова функций.
2-й случай: - Два суммирования, но у каждого свой вызов функции.
Если вы заметили ,
F2()
существует только вSum
отCase1
гдеF1()
содержится вSum
изCase1
и в обоихSum1
иSum2
отCase2
. Это станет очевидным позже, когда мы начнем делать вывод, что во втором алгоритме происходит оптимизация.Итерации по первым
Sum
вызовам casef(a)
, которые добавят к себе,f(b)
затем вызовыf(c)
, которые сделают то же самое, но добавятf(d)
к себе для каждого100000
итерации. Во втором случае мы имеемSum1
иSum2
то, и другое действует одинаково, как если бы они были одной и той же функцией, вызываемой дважды подряд.В этом случае мы можем рассматривать
Sum1
иSum2
просто как старый,Sum
гдеSum
в этом случае выглядит так:Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }
и теперь это выглядит как оптимизация, где мы можем просто считать, что это та же самая функция.Резюме с аналогией
С тем, что мы видели во втором случае, это выглядит почти так, как будто есть оптимизация, так как оба цикла имеют одинаковую точную сигнатуру, но это не является реальной проблемой. Проблема не в работе, которая выполняется
f(a)
,f(b)
,f(c)
иf(d)
. В обоих случаях и при сравнении между ними разница в расстоянии, которое должно пройти суммирование в каждом случае, дает вам разницу во времени выполнения.Подумайте о
For Loops
как являющийся ,Summations
что делает итерации , как быть ,Boss
что дает приказ двум людямA
и ,B
и что их рабочие места для мясаC
и ,D
соответственно , и , чтобы забрать некоторые пакет из них и вернуть его. В этой аналогии сами циклы for или итерации суммирования и проверки условий фактически не представляютBoss
. То, что на самом деле представляет,Boss
не непосредственно из фактических математических алгоритмов, а из фактической концепцииScope
иCode Block
внутри подпрограммы или подпрограммы, метода, функции, единицы перевода и т. Д. Первый алгоритм имеет 1 область действия, где 2-й алгоритм имеет 2 последовательных области действия.В первом случае в каждом вызове квитанция
Boss
отправляетсяA
и дает заказ иA
уходит, чтобы получитьB's
пакет, а затемBoss
отправляется вC
и дает приказы сделать то же самое и получить пакетD
на каждой итерации.Во втором случае
Boss
работает непосредственно,A
чтобы пойти и получитьB's
пакет, пока все пакеты не будут получены. ЗатемBoss
работает,C
чтобы сделать то же самое для получения всехD's
пакетов.Поскольку мы работаем с 8-байтовым указателем и имеем дело с распределением кучи, давайте рассмотрим следующую проблему. Скажем,
Boss
это 100 футов отA
и этоA
500 футов отC
. Нам не нужно беспокоиться о том, как далеко ониBoss
изначально находятся из-C
за порядка выполнения. В обоих случаяхBoss
изначально путешествует изA
затем кB
. Эта аналогия не говорит о том, что это расстояние является точным; это просто полезный сценарий тестового примера, демонстрирующий работу алгоритмов.Во многих случаях при распределении кучи и работе с кешем и файлами подкачки эти расстояния между адресами могут не сильно отличаться или могут значительно различаться в зависимости от характера типов данных и размеров массива.
Тестовые случаи:
Первый случай: на первой итерации
Boss
он должен сначала пройти 100 футов, чтобы сдать ордер,A
иA
уйти ивыполнитьсвое дело, но затемBoss
он должен пройти 500 футов,C
чтобы дать ему промах. Затем на следующей итерации и на каждой другой итерации послеBoss
нее нужно идти вперед и назад на 500 футов между ними.Второй случай:
Boss
должен проехать 100 футов на первой итерации кA
, но после этого, он уже тами только ждетA
чтобы вернутьсяпока все промахи не будут заполнены. ЗатемBoss
он должен пройти 500 футов на первой итерации,C
потому чтоC
это 500 футов отA
. Так как этоBoss( Summation, For Loop )
вызывается сразу после работы с ним,A
он просто ждет там, как он делал,A
пока всеC's
ордера не будут выполнены.Разница в пройденных расстояниях
Сравнение произвольных значений
Мы легко видим, что 600 - это гораздо меньше, чем 10 миллионов. Теперь, это не точно, потому что мы не знаем фактической разницы в расстоянии между тем, какой адрес ОЗУ или какой файл кэша или файла подкачки будет вызывать каждый вызов на каждой итерации из-за многих других невидимых переменных. Это просто оценка ситуации, которую нужно знать, и смотреть на нее с наихудшего сценария.
Из этих чисел это будет выглядеть так, как будто Алгоритм Один должен быть
99%
медленнее, чем Алгоритм Два; Однако, это толькоBoss's
часть или ответственность алгоритмов и не учитывает реальных рабочихA
,B
,C
, иD
и что они должны делать на каждом и каждой итерации цикла. Таким образом, работа босса составляет только около 15-40% всей выполняемой работы. Большая часть работы, выполняемой рабочими, оказывает несколько большее влияние на сохранение отношения разницы скоростей примерно до 50-70%.Наблюдение: - Различия между двумя алгоритмами
В этой ситуации это структура процесса выполняемой работы. Это показывает, что Случай 2 более эффективен как при частичной оптимизации, так и при наличии аналогичного объявления и определения функции, где только переменные отличаются по имени и пройденному расстоянию.
Мы также видим, что общее расстояние, пройденное в случае 1 , намного больше, чем в случае 2, и мы можем считать это расстояние пройденным нашим Фактором времени между двумя алгоритмами. Дело 1 требует гораздо больше работы, чем дело 2 .
Это
ASM
видно из свидетельств инструкций, которые были показаны в обоих случаях. Наряду с тем, что уже было сказано об этих случаях, это не учитывает тот факт, что в случае 1 боссу придется ждать обоихA
иC
вернуться, прежде чем он сможет вернуться кA
каждой следующей итерации. Это также не учитывает тот факт, что еслиA
илиB
занимает очень много времени, то обаBoss
и другие работники бездействуют в ожидании выполнения.В случае 2 бездействует только один,
Boss
пока работник не вернется. Так что даже это влияет на алгоритм.Измененные вопросы ОП
Относительно этих вопросов
Без сомнения, я продемонстрировал, что существует основная проблема еще до того, как в нее вступят аппаратное и программное обеспечение.
Теперь что касается управления памятью и кэшированием вместе с файлами подкачки и т. Д., Которые все работают вместе в интегрированном наборе систем между следующими:
The Architecture
{Аппаратное обеспечение, прошивка, некоторые встроенные драйверы, ядра и наборы инструкций ASM}.The OS
{Системы управления файлами и памятью, драйверы и реестр}.The Compiler
{Единицы перевода и оптимизация исходного кода}.Source Code
сам с его набором (-ами) отличительных алгоритмов.Мы уже видим , что есть узкое место, что происходит в первом алгоритме , прежде чем мы даже применить его к любой машине с любым произвольным
Architecture
,OS
и поProgrammable Language
сравнению со вторым алгоритмом. Там уже существовала проблема, прежде чем задействовать внутренности современного компьютера.Конечные результаты
Однако; Нельзя сказать, что эти новые вопросы не важны, потому что они сами по себе и играют роль в конце концов. Они действительно влияют на процедуры и общую производительность, и это видно из различных графиков и оценок от многих, которые дали свои ответы и / или комментарии.
Если вы обратили внимание на аналогию
Boss
и двое рабочихA
иB
которые должны были идти и получать пакетыC
и ,D
соответственно , и с учетом математических обозначений двух алгоритмов в вопросе; Вы можете увидеть без участия компьютерного оборудования и программного обеспеченияCase 2
примерно60%
быстрее, чемCase 1
.Когда вы смотрите на графики и диаграммы после того, как эти алгоритмы были применены к некоторому исходному коду, скомпилированы, оптимизированы и выполнены через ОС для выполнения их операций на данном аппаратном элементе, вы можете даже увидеть немного большее ухудшение между различиями в этих алгоритмах.
Если
Data
набор довольно мал, на первый взгляд может показаться, что разница не так уж и плоха. Тем не менее, посколькуCase 1
это примерно60 - 70%
медленнее, чемCase 2
мы можем посмотреть на рост этой функции с точки зрения различий во времени выполнения:Это приближение представляет собой среднюю разницу между этими двумя циклами как алгоритмически, так и машинными операциями, включающими оптимизацию программного обеспечения и машинные инструкции.
Когда набор данных растет линейно, увеличивается и разница во времени между ними. Алгоритм 1 имеет больше выборок, чем алгоритм 2, что очевидно, когда
Boss
он должен перемещаться назад и вперед по максимальному расстоянию междуA
&C
для каждой итерации после первой итерации, в то время как алгоритм 2Boss
должен проходитьA
один раз, а затем, после того как он закончил,A
он должен путешествовать максимальное расстояние только один раз при переходе отA
кC
.Попытка
Boss
сосредоточиться на выполнении двух одинаковых вещей одновременно и подтасовывать их взад и вперед вместо того, чтобы концентрироваться на похожих последовательных задачах, в конце концов рассердит его, так как ему пришлось путешествовать и работать вдвое больше. Поэтому не теряйте масштаб ситуации, позволяя вашему боссу попасть в интерполированное узкое место, потому что супруга и дети босса этого не оценят.Поправка: Принципы разработки программного обеспечения
- Разница между
Local Stack
иHeap Allocated
вычисления в итерационных для петель и разница между их использований, их эффективность и эффективность -Математический алгоритм, который я предложил выше, в основном применяется к циклам, которые выполняют операции с данными, размещенными в куче.
Поэтому, когда вы работаете с данными, которые должны быть в куче, и проходите через них в циклах, более эффективно хранить каждый набор данных и соответствующие ему алгоритмы в пределах своего отдельного цикла. Вы получите лучшую оптимизацию по сравнению с попыткой выделить последовательные циклы, поместив несколько операций различных наборов данных, находящихся в куче, в один цикл.
Это можно делать с данными, находящимися в стеке, поскольку они часто кэшируются, но не для данных, для которых адрес памяти запрашивается при каждой итерации.
Именно здесь вступают в игру инженерия программного обеспечения и проектирование архитектуры программного обеспечения. Это способность знать, как организовать ваши данные, знать, когда кэшировать ваши данные, знать, когда размещать ваши данные в куче, знать, как разрабатывать и реализовывать ваши алгоритмы, и знать, когда и где их вызывать.
У вас может быть тот же алгоритм, который относится к одному и тому же набору данных, но вам может потребоваться один дизайн реализации для его варианта стека, а другой - для его варианта с распределенной кучей только из-за вышеуказанной проблемы, которая видна из-за
O(n)
сложности алгоритма при работе с кучей.Из того, что я заметил за многие годы, многие люди не принимают этот факт во внимание. Они будут стремиться разработать один алгоритм, который работает с конкретным набором данных, и будут использовать его независимо от того, локально ли кэширован набор данных в стеке или если он был выделен в куче.
Если вы хотите истинную оптимизацию, да, это может показаться дублированием кода, но в целом было бы более эффективно иметь два варианта одного и того же алгоритма. Один для стековых операций, а другой для операций с кучей, которые выполняются в итерационных циклах!
Вот псевдо-пример: две простые структуры, один алгоритм.
Это то, что я имел в виду, имея отдельные реализации для стековых и кучных вариантов. Сами алгоритмы не имеют большого значения, это циклические структуры, в которых вы будете их использовать.
источник
Это может быть старый C ++ и оптимизации. На моем компьютере я получил почти такую же скорость:
Один цикл: 1,577 мс
Два цикла: 1,507 мс
Я использую Visual Studio 2015 на процессоре E5-1620 с частотой 3,5 ГГц и 16 ГБ ОЗУ.
источник