Более быстрый способ инициализации массивов с помощью умножения пустой матрицы? (Матлаб)


я наткнулся на странный способ (на мой взгляд), что 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

vs:

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.2 Ghz...)

EDIT:

прочитав ваши отзывы, я более внимательно изучил эту особенность и протестировал на 2 разных компьютерах (тот же 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]');

кто-нибудь из вас тоже такое?

EDIT #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) случае.
5 56

5 ответов:

это странно, я вижу, что 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

EDIT можете ли вы добавить + 1 для конца f и g, и посмотреть, какое время вы получаете.

EDIT Jan 6, 2013 7: 42 EST

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

машина конфиг:

i7 920. 2.653 ГГц. Линукс. 12 ГБ ОПЕРАТИВНОЙ ПАМЯТИ. 8 МБ кэш.

Graph generated on i7 920

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

я буду исследовать немного больше, прежде чем прийти спина.

редактировать 11 января 2013

после сообщения @EitanT, я хотел сделать немного больше копать. Я написал некоторый код C, чтобы увидеть, как matlab может создавать матрицу нулей. Вот код c++, который я использовал.

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 при меньших размерах (malloc + memset, в то время как g() (нули(m, 0) * нули(0, n)) продолжает использовать calloc.

таким образом, расхождение объясняется следующим

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

это поведение на Linux. Может ли кто-то сделать тот же эксперимент на другой машине (и, возможно, на другой ОС) и посмотреть, выполняется ли эксперимент ?

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

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

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

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

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 фактически является массивом неинициализированных ячеек (массив нулевых указателей), в то время как b - это массив ячеек, где каждая ячейка является пустой массив [] (внутренне и из-за обмена данными, только первая ячейка b{1} указывает на [] в то время как все остальные имеют ссылку на первую ячейку). Последний массив c похож на a (неинициализированные ячейки), но с последним содержит пустую числовую матрицу [].


я просмотрел список экспортированных функций C из libmx.dll (через Dependency Walker), и я нашел несколько интересных вещей.

  • существуют недокументированные функции для создания неинициализированных массивов:mxCreateUninitDoubleMatrix,mxCreateUninitNumericArray и mxCreateUninitNumericMatrix. На самом деле есть представление о Обмен использует эти функции для обеспечения более быстрого альтернатива

после проведения некоторых исследований, я нашел в этой статье на "Недокументированный Matlab", в котором Г-Н Яир Альтман уже пришел к выводу, что MathWork способ предварительного распределения матриц используя zeros(M, N) действительно не самый эффективный способ.

он приурочен x = zeros(M,N) и clear x, x(M,N) = 0 и обнаружил, что последний ~в 500 раз быстрее. Согласно его объяснению, второй метод просто создает M-by-N матрица, элементы которой автоматически инициализируются до 0. Однако первый метод создает xx с автоматическими нулевыми элементами), а затем присваивает ноль каждому элементу в x опять же, и это избыточная операция, которая занимает больше времени.

в случае умножения пустой матрицы, например, что вы показали в своем вопросе, MATLAB ожидает, что продукт будет матрицей m×N, и поэтому он выделяет матрицу M×N. Следовательно, выходная матрица автоматически инициализируется до нуля. Поскольку исходные матрицы пусты, дальнейшие вычисления не выполняются, и, следовательно, элементы в выходной матрице остаются неизменными и равными нулю.

интересный вопрос, по-видимому, есть несколько способов, чтобы "бить" встроенный

этот ответ фальсифицирован. Хранится только для записи. Кажется, matlab решает использовать разреженных матриц, когда он получил команду z(n,n)=0; когда n достаточно большой. Внутренняя реализация может быть в виде условия, например: (если newsize>THRESHOLD+oldsize, то используйте sparse...) где порог - это ваш "пороговый размер".

однако, это несмотря на Mathworks утверждая: "Matlab никогда создает разреженные матрицы автоматически" (ссылке)

У меня нет Matlab, чтобы проверить это. Тем не менее, использование разреженных матриц (внутренне) является более коротким объяснением (бритва Оккама), следовательно, лучше до фальсификации.