MATLAB: Сохранение нескольких переменных в "-v7.3" (HDF5).mat файлах, кажется, быстрее при использовании флага "-append". Как так?

ПРИМЕЧАНИЕ.. Этот вопрос касается проблемы, наблюдаемой еще в 2011 году со старой версией MATLAB (R2009a). Согласно обновлению, приведенному ниже с июля 2016 года, проблема/ошибка в MATLAB, похоже, больше не существует (протестировано с R2016a, прокрутите страницу до конца вопроса, чтобы увидеть обновление).

Я использую MATLAB R2009b, и мне нужно написать более крупный script, который преобразует содержимое большего набора .zip файлов в файлы макетов v7.3 (с базовой HDF5-datamodel). Чтение в порядке. Проблема заключается в экономии. И на самом деле проблем нет. Мои файлы хорошо сохраняются с помощью команды сохранить.

Мой вопрос больше в том смысле: Почему я наблюдаю следующее удивительное (для меня) поведение в MATLAB?

посмотрим на мою проблему в целом. В текущем тестовом сценарии я буду генерировать один результат: матовый файл A -v7.3. Этот .mat файл будет содержать 40 блоков в качестве отдельных переменных. Каждая переменная будет называться "block_NNN" от 1 до 40 и будет содержать структуру с фреймами полей и blockNo. Полевые рамки содержат последовательность изображений uint8 480x240x65 (здесь только случайные данные, созданные с помощью randi). Блок поляNo содержит номер блока.

Примечание: в реальном script (которого я еще не закончил) я буду делать выше всего 370 раз, конвертируя в общей сложности 108 ГБ необработанных данных. Вот почему я заинтересован в следующем.

Во всяком случае, сначала я определяю некоторые общие переменные:

% some sizes for dummy data and loops:
num_blockCount = 40;
num_blockLength = 65;
num_frameHeight = 480;
num_frameWidth = 240;

Затем я создаю некоторый фиктивный код, который имеет форму и размер, идентичные фактическим исходным данным:

% generate empty struct:
stu_data2disk = struct();

% loop over blocks:
for num_k = 1:num_blockCount

   % generate block-name:
   temp_str_blockName = sprintf('block_%03u', num_k);

   % generate temp struct for current block:
   temp_stu_value = struct();
   temp_stu_value.frames = randi( ...
      [0 255], ...
      [num_frameHeight num_frameWidth num_blockLength], ...
      'uint8' ...
   );
   temp_stu_value.blockNo = num_k;

   % using dynamic field names:
   stu_data2disk.(sprintf('block_%03u', num_k)) = temp_stu_value;

end

Теперь у меня есть все мои случайные тестовые данные в struct stu_data2disk. Теперь я хотел бы сохранить данные, используя один из двух возможных методов.

Сначала попробуйте простой:

% save data (simple):
disp('Save data the simple way:')
tic;
save converted.mat -struct stu_data2disk -v7.3;
toc;

Файл написан без проблем (286 МБ). Выход:

Save data the simple way:
Elapsed time is 14.004449 seconds.

ОК - тогда я вспомнил, что хотел бы следовать процедуре сохранения по 40 блокам. Таким образом, вместо вышеперечисленного я петлю над блоками и добавляю их последовательно:

% save to file, using append:
disp('Save data using -append:')
tic;
for num_k = 1:num_blockCount

   % generate block-name:
   temp_str_blockName = sprintf('block_%03u', num_k);

   temp_str_appendToggle = '';
   if (num_k > 1)
      temp_str_appendToggle = '-append';
   end

   % generate save command:
   temp_str_saveCommand = [ ...
      'save ', ...
      'converted_append.mat ', ...
      '-struct stu_data2disk ', temp_str_blockName, ' '...
      temp_str_appendToggle, ' ', ...
      '-v7.3', ...
      ';' ...
   ];

   % evaluate save command:
   eval(temp_str_saveCommand);

end
toc;

И снова файл сохраняет красиво (286 МБ). Выход:

Save data using -append:
Elapsed time is 0.956968 seconds.

Интересно, что метод append намного быстрее? Мой вопрос: почему?

Выход из dir converted*.mat:

09-02-2011  20:38       300,236,392 converted.mat
09-02-2011  20:37       300,264,316 converted_append.mat
               2 File(s)    600,500,708 bytes

Файлы не идентичны по размеру. И тест с fc в Windows 7 показал... много многозначных различий. Возможно, данные немного сдвинуты - это ничего нам не говорит.

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

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

[EDIT]: я просто попытался использовать флаг формата (по умолчанию - -v7 в моей системе), и больше нет никакой разницы:

Save data the simple way (-v7):
Elapsed time is 13.092084 seconds.
Save data using -append (-v7):
Elapsed time is 14.345314 seconds.

[EDIT]: Я исправил ошибку выше. Раньше я упоминал, что статистика была для -v6, но я ошибся. Я только что удалил флаг формата и предположил, что по умолчанию был -v6, но на самом деле это -v7.

Я создал новую тестовую статистику для всех форматов в моей системе, используя прекрасную фреймворк Andrew (все форматы предназначены для тех же случайных данных теста, которые теперь читаются из файла):

15:15:51.422: Testing speed, format=-v6, R2009b on PCWIN, arch=x86, os=Microsoft Windows 7 Professional  6.1.7600 N/A Build 7600
15:16:00.829: Save the simple way:            0.358 sec
15:16:01.188: Save using multiple append:     7.432 sec
15:16:08.614: Save using one big append:      1.161 sec

15:16:24.659: Testing speed, format=-v7, R2009b on PCWIN, arch=x86, os=Microsoft Windows 7 Professional  6.1.7600 N/A Build 7600
15:16:33.442: Save the simple way:           12.884 sec
15:16:46.329: Save using multiple append:    14.442 sec
15:17:00.775: Save using one big append:     13.390 sec

15:17:31.579: Testing speed, format=-v7.3, R2009b on PCWIN, arch=x86, os=Microsoft Windows 7 Professional  6.1.7600 N/A Build 7600
15:17:40.690: Save the simple way:           13.751 sec
15:17:54.434: Save using multiple append:     3.970 sec
15:17:58.412: Save using one big append:      6.138 sec

И размеры файлов:

10-02-2011  15:16       299,528,768 converted_format-v6.mat
10-02-2011  15:16       299,528,768 converted_append_format-v6.mat
10-02-2011  15:16       299,528,832 converted_append_batch_format-v6.mat
10-02-2011  15:16       299,894,027 converted_format-v7.mat
10-02-2011  15:17       299,894,027 converted_append_format-v7.mat
10-02-2011  15:17       299,894,075 converted_append_batch_format-v7.mat
10-02-2011  15:17       300,236,392 converted_format-v7.3.mat
10-02-2011  15:17       300,264,316 converted_append_format-v7.3.mat
10-02-2011  15:18       300,101,800 converted_append_batch_format-v7.3.mat
               9 File(s)  2,698,871,005 bytes

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

Хм, возможно, какая-то оптимизация в базовых функциях записи HDF5?

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

Другие примечания:

Поведение очень системное, как мы видим в ответе Andrew ниже. Также представляется весьма важным вопрос о том, запускаете ли вы эти вещи в локальной области функции или в "глобальном" m- script. Мои первые результаты были получены из m- script, где файлы были записаны в текущий каталог. Я все еще могу воспроизвести 1-секундную запись для -7.3 в m- script. Функциональные вызовы, по-видимому, добавляют некоторые служебные данные.

Обновление июля 2016 года:

Я снова нашел это и подумал, что могу проверить его с помощью новейшего MATLAB, доступного мне на данный момент. С MATLAB R2016a в Windows 7 x64 проблема, похоже, исправлена:

14:04:06.277: Testing speed, imax=255, R2016a on PCWIN64, arch=AMD64, 16 GB, os=Microsoft Windows 7 Enterprise  Version 6.1 (Build 7601: Service Pack 1)
14:04:10.600: basic -v7.3:                    7.599 sec      5.261 GB used
14:04:18.229: basic -v7.3:                    7.894 sec      5.383 GB used
14:04:26.154: basic -v7.3:                    7.909 sec      5.457 GB used
14:04:34.096: basic -v7.3:                    7.919 sec      5.498 GB used
14:04:42.048: basic -v7.3:                    7.886 sec      5.516 GB used     286 MB file   7.841 sec mean
14:04:50.581: multiappend -v7.3:              7.928 sec      5.819 GB used
14:04:58.544: multiappend -v7.3:              7.905 sec      5.834 GB used
14:05:06.485: multiappend -v7.3:              8.013 sec      5.844 GB used
14:05:14.542: multiappend -v7.3:              8.591 sec      5.860 GB used
14:05:23.168: multiappend -v7.3:              8.059 sec      5.868 GB used     286 MB file   8.099 sec mean
14:05:31.913: bigappend -v7.3:                7.727 sec      5.837 GB used
14:05:39.676: bigappend -v7.3:                7.740 sec      5.879 GB used
14:05:47.453: bigappend -v7.3:                7.645 sec      5.884 GB used
14:05:55.133: bigappend -v7.3:                7.656 sec      5.877 GB used
14:06:02.824: bigappend -v7.3:                7.963 sec      5.871 GB used     286 MB file   7.746 sec mean

Это было проверено с помощью функции Andrew Janke reproMatfileAppendSpeedup в принятом ответе ниже (5 проходов с форматом 7.3). Теперь -append будет одинаково медленным или медленным до одного сохранения - как и должно быть. Возможно, это была проблема с ранней сборкой драйвера HDF5, используемого в R2009a.

Ответ 1

Святая корова. Я могу воспроизвести. Пробовал вариант с одним добавлением; он еще быстрее. Похоже, что "-append" просто волшебным образом делает экономию на основе HDF5() на 30 раз быстрее. У меня нет объяснений, но я хотел поделиться тем, что нашел.

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

Не вижу большого ускорения во всем мире. Это огромно для моего 64-битного пакета XP и 32-разрядного окна Server 2003, большого в моем 64-битном окне Windows 7, несуществующего в 32-разрядной версии XP. (Хотя несколько приложений являются огромными потерями на Server 2003.) R2010b во многих случаях медленнее. Может быть, HDF5 добавит или сохранит его использование, просто скачав на новых версиях Windows. (XP x64 на самом деле является ядром Server 2003). Или, может быть, это просто разница в конфигурации машины. Там быстрый RAID на компьютере XP x64, а 32-разрядный XP имеет меньше оперативной памяти, чем остальные. Какие ОС и архитектура вы используете? Вы также можете попробовать этот репер?

19:36:40.289: Testing speed, format=-v7.3, R2009b on PCWIN64, arch=AMD64, os=Microsoft(R) Windows(R) XP Professional x64 Edition 5.2.3790 Service Pack 2 Build 3790
19:36:55.930: Save the simple way:           11.493 sec
19:37:07.415: Save using multiple append:     1.594 sec
19:37:09.009: Save using one big append:      0.424 sec


19:39:21.681: Testing speed, format=-v7.3, R2009b on PCWIN, arch=x86, os=Microsoft Windows XP Professional 5.1.2600 Service Pack 3 Build 2600
19:39:37.493: Save the simple way:           10.881 sec
19:39:48.368: Save using multiple append:    10.187 sec
19:39:58.556: Save using one big append:     11.956 sec


19:44:33.410: Testing speed, format=-v7.3, R2009b on PCWIN64, arch=AMD64, os=Microsoft Windows 7 Professional  6.1.7600 N/A Build 7600
19:44:50.789: Save the simple way:           14.354 sec
19:45:05.156: Save using multiple append:     6.321 sec
19:45:11.474: Save using one big append:      2.143 sec


20:03:37.907: Testing speed, format=-v7.3, R2009b on PCWIN, arch=x86, os=Microsoft(R) Windows(R) Server 2003, Enterprise Edition 5.2.3790 Service Pack 2 Build 3790
20:03:58.532: Save the simple way:           19.730 sec
20:04:18.252: Save using multiple append:    77.897 sec
20:05:36.160: Save using one big append:      0.630 sec

Это выглядит огромным. Если он держится на других наборах данных, я могу использовать этот трюк во многих местах. Возможно, что-то и с MathWorks. Могут ли они использовать технику быстрого добавления в обычных сбережениях или других версиях ОС?

Здесь имеется автономная функция воспроизведения.

function out = reproMatfileAppendSpeedup(nPasses, tests, imax, formats)
%REPROMATFILEAPPENDSPEEDUP Show how -append makes v7.3 saves much faster
%
% Examples:
% reproMatfileAppendSpeedup()
% reproMatfileAppendSpeedup(2, [], 0, {'7.3','7','6'}); % low-entropy test

if nargin < 1 || isempty(nPasses);  nPasses = 1;  end
if nargin < 2 || isempty(tests);    tests = {'basic','multiappend','bigappend'}; end
if nargin < 3 || isempty(imax);     imax = 255; end
if nargin < 4 || isempty(formats);  formats = '7.3'; end % -v7 and -v6 do not show the speedup
tests = cellstr(tests);
formats = cellstr(formats);

fprintf('%s: Testing speed, imax=%d, R%s on %s\n',...
    timestamp, imax, version('-release'), systemDescription());

tempDir = setupTempDir();
testData = generateTestData(imax);

testMap = struct('basic','saveSimple', 'multiappend','saveMultiAppend', 'bigappend','saveBigAppend');

for iFormat = 1:numel(formats)
    format = formats{iFormat};
    formatFlag = ['-v' format];
    %fprintf('%s: Format %s\n', timestamp, formatFlag);
    for iTest = 1:numel(tests)
        testName = tests{iTest};
        saveFcn = testMap.(testName);
        te = NaN(1, nPasses);
        for iPass = 1:nPasses
            fprintf('%s: %-30s', timestamp, [testName ' ' formatFlag ':']);
            t0 = tic;
            matFile = fullfile(tempDir, sprintf('converted-%s-%s-%d.mat', testName, format, i));
            feval(saveFcn, matFile, testData, formatFlag);
            te(iPass) = toc(t0);
            if iPass == nPasses
                fprintf('%7.3f sec      %5.3f GB used   %5.0f MB file   %5.3f sec mean\n',...
                    te(iPass), physicalMemoryUsed/(2^30), getfield(dir(matFile),'bytes')/(2^20), mean(te));
            else
                fprintf('%7.3f sec      %5.3f GB used\n', te(iPass), physicalMemoryUsed/(2^30));
            end
        end
        % Verify data to make sure we are sane
        gotBack = load(matFile);
        gotBack = rmfield(gotBack, intersect({'dummy'}, fieldnames(gotBack)));
        if ~isequal(gotBack, testData)
            fprintf('ERROR: Loaded data differs from original for %s %s\n', formatFlag, testName);
        end
    end
end

% Clean up
rmdir(tempDir, 's');

%%
function saveSimple(file, data, formatFlag)
save(file, '-struct', 'data', formatFlag);

%%
function out = physicalMemoryUsed()
if ~ispc
    out = NaN;
    return; % memory() only works on Windows
end
[u,s] = memory();
out = s.PhysicalMemory.Total - s.PhysicalMemory.Available;

%%
function saveBigAppend(file, data, formatFlag)
dummy = 0;
save(file, 'dummy', formatFlag);
fieldNames = fieldnames(data);
save(file, '-struct', 'data', fieldNames{:}, '-append', formatFlag);

%%
function saveMultiAppend(file, data, formatFlag)
fieldNames = fieldnames(data);
for i = 1:numel(fieldNames)
    if (i > 1); appendFlag = '-append'; else; appendFlag = ''; end
    save(file, '-struct', 'data', fieldNames{i}, appendFlag, formatFlag);
end


%%
function testData = generateTestData(imax)
nBlocks = 40;
blockSize = [65 480 240];
for i = 1:nBlocks
    testData.(sprintf('block_%03u', i)) = struct('blockNo',i,...
        'frames', randi([0 imax], blockSize, 'uint8'));
end

%%
function out = timestamp()
%TIMESTAMP Showing timestamps to make sure it is not a tic/toc problem
out = datestr(now, 'HH:MM:SS.FFF');

%%
function out = systemDescription()
if ispc
    platform = [system_dependent('getos'),' ',system_dependent('getwinsys')];
elseif ismac
    [fail, input] = unix('sw_vers');
    if ~fail
        platform = strrep(input, 'ProductName:', '');
        platform = strrep(platform, sprintf('\t'), '');
        platform = strrep(platform, sprintf('\n'), ' ');
        platform = strrep(platform, 'ProductVersion:', ' Version: ');
        platform = strrep(platform, 'BuildVersion:', 'Build: ');
    else
        platform = system_dependent('getos');
    end
else
    platform = system_dependent('getos');
end
arch = getenv('PROCESSOR_ARCHITEW6432');
if isempty(arch)
    arch = getenv('PROCESSOR_ARCHITECTURE');
end
try
    [~,sysMem] = memory();
catch
    sysMem.PhysicalMemory.Total = NaN;
end
out = sprintf('%s, arch=%s, %.0f GB, os=%s',...
    computer, arch, sysMem.PhysicalMemory.Total/(2^30), platform);

%%
function out = setupTempDir()
out = fullfile(tempdir, sprintf('%s - %s', mfilename, datestr(now, 'yyyymmdd-HHMMSS-FFF')));
mkdir(out);

EDIT: я изменил функцию воспроизведения, добавив несколько итераций и параметризуя ее для сохранения стилей, форматов файлов и imax для генератора randi.

Я думаю, что кэширование файловой системы является большим фактором для поведения fast -append. Когда я делаю кучу прогонов подряд с параметром reproMatfileAppendSpeedup (20) и просматриваю Системную информацию в Process Explorer, большинство из них находятся под второй, а использование физической памяти быстро увеличивается на пару ГБ. Затем каждый десяток проходит, пишет киоски и занимает 20 или 30 секунд, а физическое использование ОЗУ медленно приближается к тому, где оно началось. Я думаю, это означает, что Windows кэширует много записей в оперативной памяти, и что-то о -append делает это более желательным. Но амортизированное время, в том числе и те киоски, по-прежнему намного быстрее, чем основная экономия для меня.

Кстати, после нескольких проходов в течение пары часов мне нелегко воспроизводить исходные тайминги.

Ответ 2

Просто обновление в случае, если оно полезно другим. Я нашел ошибку Matlab 784028, которая показывает, что без сжатия для -append поведение фиксировано из 2012a. Из некоторых тестов в моей системе это действительно так, сжатие происходит для переменных > 10000 байт с использованием или без использования append и никогда не бывает для меньших переменных.

К сожалению, обратная сторона этого заключается в том, что нет никакого способа управления сжатием с -v7.3 файлами.

Ответ 3

Эксперименты, сделанные @AndrewJanke, очень интересны. Следует помнить, что три формата MAT файлов, которые вы сравниваете, совершенно разные: v6 несжатый, v7 сжат, а v7.3 также сжат, но использует совершенно другую реализацию (стандартный формат HDF5 и пользовательский формат, оптимизированный для MATLAB).

Что касается сравнения "save-all-vars-at-once vs. append-one-var-at-time", я также удивлен результатами...