Я наткнулся на странный способ (на мой взгляд), что Matlab имеет дело с пустыми матрицами. Например, если две пустые матрицы умножаются, результат:
zeros(3,0)*zeros(0,3)
ans =
0 0 0
0 0 0
0 0 0
Теперь это уже застало меня врасплох, однако быстрый поиск привлек меня к ссылке выше, и я получил объяснение несколько искаженной логики, почему это происходит.
Однако, ничто не подготовило меня к следующему наблюдению. Я спросил себя: насколько эффективен этот тип умножения и просто используется функция zeros(n)
, например, для инициализации? Я использовал timeit
, чтобы ответить на это:
f=@() zeros(1000)
timeit(f)
ans =
0.0033
против
g=@() zeros(1000,0)*zeros(0,1000)
timeit(g)
ans =
9.2048e-06
Оба имеют одинаковый результат матрицы 1000x1000 нулей класса double
, но пустая матрица умножается на ~ 350 раз быстрее! (аналогичный результат происходит с использованием tic
и toc
и цикла)
Как это может быть? timeit
или tic,toc
блеф или я нашел более быстрый способ инициализации матриц?
(это было сделано с помощью matlab 2012a, на машине win7-64, intel-i5 650 3.2Ghz...)
EDIT:
После прочтения ваших отзывов я более тщательно рассмотрел эту особенность и протестировал на двух разных компьютерах (тот же самый matlab ver, хотя 2012a) код, который проверяет время выполнения и размер матрицы n. Это то, что я получаю:
Код для генерации этого используемого timeit
, как и раньше, но цикл с tic
и toc
будет выглядеть одинаково. Таким образом, для небольших размеров zeros(n)
сравнимо. Однако вокруг n=400
наблюдается скачок производительности для пустого матричного умножения. Код, который я использовал для создания этого сюжета, был:
n=unique(round(logspace(0,4,200)));
for k=1:length(n)
f=@() zeros(n(k));
t1(k)=timeit(f);
g=@() zeros(n(k),0)*zeros(0,n(k));
t2(k)=timeit(g);
end
loglog(n,t1,'b',n,t2,'r');
legend('zeros(n)','zeros(n,0)*zeros(0,n)',2);
xlabel('matrix size (n)'); ylabel('time [sec]');
Вы тоже испытываете это?
РЕДАКТИРОВАТЬ № 2:
Кстати, пустое матричное умножение не требуется для получения этого эффекта. Можно просто сделать:
z(n,n)=0;
где n > некоторый пороговый размер матрицы, рассматриваемый на предыдущем графике, и получить профиль эффективности точный, как с пустым умножением матрицы (снова используя timeit).
Здесь пример, где он повышает эффективность кода:
n = 1e4;
clear z1
tic
z1 = zeros( n );
for cc = 1 : n
z1(:,cc)=cc;
end
toc % Elapsed time is 0.445780 seconds.
%%
clear z0
tic
z0 = zeros(n,0)*zeros(0,n);
for cc = 1 : n
z0(:,cc)=cc;
end
toc % Elapsed time is 0.297953 seconds.
Однако использование z(n,n)=0;
приводит к аналогичным результатам в случае zeros(n)
.
Это странно, я вижу, что f быстрее, а g медленнее, чем вы видите. Но оба они одинаковы для меня. Возможно, другая версия MATLAB?
>> g = @() zeros(1000, 0) * zeros(0, 1000);
>> f = @() zeros(1000)
f =
@()zeros(1000)
>> timeit(f)
ans =
8.5019e-04
>> timeit(f)
ans =
8.4627e-04
>> timeit(g)
ans =
8.4627e-04
РЕДАКТИРОВАТЬ, вы можете добавить + 1 для конца f и g и посмотреть, сколько времени вы получаете.
EDIT 6 января 2013 г. 7:42 EST
Я использую машину удаленно, поэтому извините за низкокачественные графики (их нужно было скрыть).
Конфигурация машины:
i7 920. 2.653 ГГц. Linux. 12 ГБ оперативной памяти. Кеш 8 МБ.
Похоже, что даже машина, к которой у меня есть доступ, показывает это поведение, за исключением большего размера (где-то между 1979 и 2073 годами). Нет причин, по которым я могу сейчас думать о том, что пустая матрица умножается быстрее при больших размерах.
Я буду изучать немного больше, прежде чем возвращаться.
РЕДАКТИРОВАТЬ 11 января 2013 г.
После сообщения @EitanT я хотел немного поработать. Я написал некоторый C-код, чтобы увидеть, как Matlab может создавать матрицу нулей. Вот код С++, который я использовал.
int main(int argc, char **argv)
{
for (int i = 1975; i <= 2100; i+=25) {
timer::start();
double *foo = (double *)malloc(i * i * sizeof(double));
for (int k = 0; k < i * i; k++) foo[k] = 0;
double mftime = timer::stop();
free(foo);
timer::start();
double *bar = (double *)malloc(i * i * sizeof(double));
memset(bar, 0, i * i * sizeof(double));
double mmtime = timer::stop();
free(bar);
timer::start();
double *baz = (double *)calloc(i * i, sizeof(double));
double catime = timer::stop();
free(baz);
printf("%d, %lf, %lf, %lf\n", i, mftime, mmtime, catime);
}
}
Вот результаты.
$ ./test
1975, 0.013812, 0.013578, 0.003321
2000, 0.014144, 0.013879, 0.003408
2025, 0.014396, 0.014219, 0.003490
2050, 0.014732, 0.013784, 0.000043
2075, 0.015022, 0.014122, 0.000045
2100, 0.014606, 0.014480, 0.000045
Как вы можете видеть, calloc
(4-й столбец) кажется самым быстрым методом. Он также значительно ускоряется между 2025 и 2050 годами (предположим, это примерно в 2048 году?).
Теперь я вернулся в Matlab, чтобы проверить то же самое. Вот результаты.
>> test
1975, 0.003296, 0.003297
2000, 0.003377, 0.003385
2025, 0.003465, 0.003464
2050, 0.015987, 0.000019
2075, 0.016373, 0.000019
2100, 0.016762, 0.000020
Похоже, что как f(), так и g() используют calloc
при меньших размерах (< 2048?). Но при больших размерах f() (нули (m, n)) начинает использовать malloc
+ memset
, а g() (zeros (m, 0) * zeros (0, n)) продолжает использовать calloc
.
Таким образом, расхождение объясняется следующим
calloc
также ведет себя несколько неожиданно, что приводит к улучшению производительности.Это поведение в Linux. Может ли кто-то сделать тот же эксперимент на другой машине (и, возможно, в другой ОС) и посмотреть, есть ли эксперимент?
g
медленнее, чем f
почти в 2 раза! ... ммм
Результаты могут быть немного обманчивыми. Когда вы умножаете две пустые матрицы, результирующая матрица не сразу "распределяется" и "инициализируется", скорее это откладывается до тех пор, пока вы ее сначала не используете (вроде как ленивая оценка).
То же самое относится к indexing за пределами вырасти переменную, которая в случае числовых массивов заполняет любые отсутствующие записи нулями (я потом обсужу нечисловой случай). Конечно, рост матрицы таким образом не перезаписывает существующие элементы.
Таким образом, хотя это может показаться более быстрым, вы просто задерживаете время распределения, пока вы на самом деле не начнете использовать матрицу. В конце вы будете иметь аналогичные тайминги, как если бы вы сделали выделение с самого начала.
Пример, чтобы показать это поведение, по сравнению с несколькими другими альтернативами:
N = 1000;
clear z
tic, z = zeros(N,N); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
clear z
tic, z = zeros(N,0)*zeros(0,N); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
clear z
tic, z(N,N) = 0; toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
clear z
tic, z = full(spalloc(N,N,0)); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
clear z
tic, z(1:N,1:N) = 0; toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
clear z
val = 0;
tic, z = val(ones(N)); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
clear z
tic, z = repmat(0, [N N]); toc
tic, z = z + 1; toc
assert(isequal(z,ones(N)))
Результат показывает, что если вы суммируете прошедшее время для обеих команд в каждом случае, вы получите аналогичные общие тайминги:
// zeros(N,N)
Elapsed time is 0.004525 seconds.
Elapsed time is 0.000792 seconds.
// zeros(N,0)*zeros(0,N)
Elapsed time is 0.000052 seconds.
Elapsed time is 0.004365 seconds.
// z(N,N) = 0
Elapsed time is 0.000053 seconds.
Elapsed time is 0.004119 seconds.
Другие тайминги:
// full(spalloc(N,N,0))
Elapsed time is 0.001463 seconds.
Elapsed time is 0.003751 seconds.
// z(1:N,1:N) = 0
Elapsed time is 0.006820 seconds.
Elapsed time is 0.000647 seconds.
// val(ones(N))
Elapsed time is 0.034880 seconds.
Elapsed time is 0.000911 seconds.
// repmat(0, [N N])
Elapsed time is 0.001320 seconds.
Elapsed time is 0.003749 seconds.
Эти измерения слишком малы в миллисекундах и могут быть не очень точными, поэтому вы можете запускать эти команды в цикле несколько тысяч раз и принимать среднее значение. Также иногда выполнение сохраненных M-функций выполняется быстрее, чем запуск сценариев или командную строку, так как определенные оптимизации происходят только так...
В любом случае выделение обычно выполняется один раз, поэтому кому это нужно, если требуется дополнительные 30 мс:)
Аналогичное поведение можно наблюдать с массивами ячеек или массивами структур. Рассмотрим следующий пример:
N = 1000;
tic, a = cell(N,N); toc
tic, b = repmat({[]}, [N,N]); toc
tic, c{N,N} = []; toc
который дает:
Elapsed time is 0.001245 seconds.
Elapsed time is 0.040698 seconds.
Elapsed time is 0.004846 seconds.
Обратите внимание, что даже если все они равны, они занимают различный объем памяти:
>> assert(isequal(a,b,c))
>> whos a b c
Name Size Bytes Class Attributes
a 1000x1000 8000000 cell
b 1000x1000 112000000 cell
c 1000x1000 8000104 cell
На самом деле ситуация здесь немного сложнее, так как MATLAB, вероятно, разделяет ту же пустую матрицу для всех ячеек, а не создание нескольких копий.
Массив ячеек a
на самом деле представляет собой массив неинициализированных ячеек (массив NULL-указателей), а b
- это массив ячеек, где каждая ячейка представляет собой пустой массив []
(внутри и из-за совместного использования данных, только первая ячейка b{1}
указывает на []
, а все остальные имеют ссылку на первую ячейку). Конечный массив c
похож на a
(неинициализированные ячейки), но с последним, содержащим пустую числовую матрицу []
.
Я просмотрел список экспортированных функций C из libmx.dll
(используя инструмент Dependency Walker), и я нашел несколько интересных вещи.
существуют недокументированные функции для создания неинициализированных массивов: mxCreateUninitDoubleMatrix
, mxCreateUninitNumericArray
и mxCreateUninitNumericMatrix
. Фактически есть представление о File Exchange использует эти функции, чтобы обеспечить более быструю альтернативу функции zeros
.
существует недокументированная функция, называемая mxFastZeros
. Googling онлайн, я могу видеть, что вы перекрестно поставили этот вопрос на MATLAB Answers, а также отличные ответы там. Джеймс Турса (тот же автор UNINIT из ранее) дал пример о том, как использовать эту недокументированную функцию.
libmx.dll
связан с tbbmalloc.dll
разделяемой библиотекой. Это масштабируемый распределитель памяти Intel TBB. Эта библиотека обеспечивает эквивалентные функции выделения памяти (malloc
, calloc
, free
), оптимизированные для параллельных приложений. Помните, что многие функции MATLAB автоматически многопоточны, поэтому я не удивлюсь, если zeros(..)
многопоточно и использует распределитель памяти Intel, как только размер матрицы достаточно велик (вот недавний комментарий Loren Shure, который подтверждает этот факт).
Что касается последнего пункта о распределителе памяти, вы можете написать аналогичный тест в C/С++, подобный тому, что было @PavanYalamanchili, и сравнить доступные распределители. Что-то вроде this. Помните, что MEX файлы имеют несколько более высокое управление памятьюнакладные расходы, поскольку MATLAB автоматически освобождает любую память, которая была выделена в MEX файлах, используя функции mxCalloc
, mxMalloc
или mxRealloc
. Для того, что стоило, в более старых версиях раньше можно было изменить внутренний менеджер памяти.
Вот более тщательный критерий для сравнения обсуждаемых альтернатив. В нем конкретно показано, что, как только вы подчеркиваете использование всей выделенной матрицы, все три метода находятся на равной основе, а разница незначительна.
function compare_zeros_init()
iter = 100;
for N = 512.*(1:8)
% ZEROS(N,N)
t = zeros(iter,3);
for i=1:iter
clear z
tic, z = zeros(N,N); t(i,1) = toc;
tic, z(:) = 9; t(i,2) = toc;
tic, z = z + 1; t(i,3) = toc;
end
fprintf('N = %4d, ZEROS = %.9f\n', N, mean(sum(t,2)))
% z(N,N)=0
t = zeros(iter,3);
for i=1:iter
clear z
tic, z(N,N) = 0; t(i,1) = toc;
tic, z(:) = 9; t(i,2) = toc;
tic, z = z + 1; t(i,3) = toc;
end
fprintf('N = %4d, GROW = %.9f\n', N, mean(sum(t,2)))
% ZEROS(N,0)*ZEROS(0,N)
t = zeros(iter,3);
for i=1:iter
clear z
tic, z = zeros(N,0)*zeros(0,N); t(i,1) = toc;
tic, z(:) = 9; t(i,2) = toc;
tic, z = z + 1; t(i,3) = toc;
end
fprintf('N = %4d, MULT = %.9f\n\n', N, mean(sum(t,2)))
end
end
Ниже приведены интервалы времени, усредненные по 100 итерациям с точки зрения увеличения размера матрицы. Я выполнил тесты в R2013a.
>> compare_zeros_init
N = 512, ZEROS = 0.001560168
N = 512, GROW = 0.001479991
N = 512, MULT = 0.001457031
N = 1024, ZEROS = 0.005744873
N = 1024, GROW = 0.005352638
N = 1024, MULT = 0.005359236
N = 1536, ZEROS = 0.011950846
N = 1536, GROW = 0.009051589
N = 1536, MULT = 0.008418878
N = 2048, ZEROS = 0.012154002
N = 2048, GROW = 0.010996315
N = 2048, MULT = 0.011002169
N = 2560, ZEROS = 0.017940950
N = 2560, GROW = 0.017641046
N = 2560, MULT = 0.017640323
N = 3072, ZEROS = 0.025657999
N = 3072, GROW = 0.025836506
N = 3072, MULT = 0.051533432
N = 3584, ZEROS = 0.074739924
N = 3584, GROW = 0.070486857
N = 3584, MULT = 0.072822335
N = 4096, ZEROS = 0.098791732
N = 4096, GROW = 0.095849788
N = 4096, MULT = 0.102148452
zeros
. Кроме того, мне нужно предварительно выделить ~ 100 раз в секунду для анализа данных на лету, поэтому я стараюсь максимально оптимизировать свой код ...
mxArray
чтобы указать это состояние. Вот почему само предварительное распределение вводит в заблуждение. Я только добавил более полное сравнение, см. Редактирование ..
После некоторых исследований я нашел эту статью в "Недокументированный Matlab" , в котором Mr. Яир Альтман уже пришел к выводу, что MathWork способ превалирования матриц с использованием zeros(M, N)
действительно не самый эффективный способ.
Он приурочил x = zeros(M,N)
против clear x, x(M,N) = 0
и обнаружил, что последний примерно в 500 раз быстрее. Согласно его объяснению, второй метод просто создает матрицу M-by-N, элементы которой автоматически инициализируются на 0. Первый метод, однако, создает x
(с x
с автоматическими нулевыми элементами), а затем назначает нуль для каждого элемента в x
снова, и это избыточная операция, которая занимает больше времени.
В случае пустого матричного умножения, такого как то, что вы указали в своем вопросе, MATLAB ожидает, что продукт будет матрицей M × N, и поэтому он выделяет матрицу M × N. Следовательно, выходная матрица автоматически инициализируется нулями. Так как исходные матрицы пустые, дальнейшие вычисления не выполняются, и, следовательно, элементы в выходной матрице остаются неизменными и равными нулю.
n>1000
?
Интересный вопрос, видимо, есть несколько способов "избить" встроенную функцию zeros
. Мое единственное предположение о том, почему это происходит, было бы в том, что он может быть более эффективным с точки зрения памяти (в конце концов, zeros(LargeNumer)
скорее приведет к тому, что Matlab достигнет предела памяти, чем сформирует узкое место в большинстве кодов) или более устойчивым.
Вот еще один метод быстрого выделения, использующий разреженную матрицу, я добавил регулярную нулевую функцию в качестве эталона:
tic; x=zeros(1000,1000); toc
Elapsed time is 0.002863 seconds.
tic; clear x; x(1000,1000)=0; toc
Elapsed time is 0.000282 seconds.
tic; x=full(spalloc(1000,1000,0)); toc
Elapsed time is 0.000273 seconds.
tic; x=spalloc(1000,1000,1000000); toc %Is this the same for practical purposes?
Elapsed time is 0.000281 seconds.
Этот ответ фальсифицирован. Держатся за запись. Кажется, что Matlab решает использовать разреженные матрицы, когда он получил команду как z(n,n)=0;
когда n достаточно велико. Внутренняя реализация может быть в виде условия вроде: (если newsize> THRESHOLD + oldsize, то используйте разреженный...), где THRESHOLD является вашим "пороговым размером".
Однако это несмотря на то, что Mathworks утверждает: "Matlab никогда не создает разреженные матрицы автоматически" (ссылка)
У меня нет Matlab, чтобы проверить это. Тем не менее, использование разреженных матриц (внутренне) является более коротким объяснением (бритва Оккама), следовательно, лучше до фальсификации.