18 мар. 2019 г.

Чтение из MS Excel. Кто быстрее?


    При написании "Заполнение страницы MS Excel одной командой" я вспомнил про библиотеки работающие с файлом MS Excel "напрямую". Посмотрим, насколько чтение информации из MS Excel "напрямую" быстрее, чем при использовании OLE.
    Создадим файл с 1 000 000 строк по 5 столбцов и прочитаем его различными способами.

Вариант 1. Чтение листа MS Excel через OLE-сервер по ячейкам
procedure ReadCom;
var
  ovEA: OleVariant;
  iRow, iCol, iRowCount, iColCount: Integer;
  dStart: TDateTime;
  iSum: Int64;
begin
  iSum := 0;
  dStart := Now;
  Write('1. Excel.Application & Cells: ');
  ovEA := CreateOleObject('Excel.Application');
  try
    ovEA.DisplayAlerts := False; // отключаем сообщения
    ovEA.EnableEvents := False; // отключаем обработку событий
    ovEA.ScreenUpdating := False; // отключаем перерисовку объектов на экране
    ovEA.DisplayStatusBar := False; // отключаем вывод в строку статуса
    ovEA.Workbooks.Open(sFileName);
    iRowCount := ovEA.ActiveSheet.UsedRange.Rows.Count;
    iColCount := ovEA.ActiveSheet.UsedRange.Columns.Count;
    for iRow := 1 to iRowCount do
      for iCol := 1 to iColCount do
        iSum := iSum + ovEA.Cells[iRow, iCol].Value;
  finally
    ovEA.Quit;
    ovEA := Unassigned;
  end;
  WriteLn(FormatDateTime('nn:ss.zzz', Now - dStart), ' Sum = ', iSum);
end;
Результаты
32-бит: 58:23.060 - Час! Час, Карл!!!
64-бит: 47:01.807 - это уже лучше
Вариант 2. Чтение листа MS Excel через OLE-сервер диапазоном ячеек
procedure ReadComAsArray;
var
  ovEA: OleVariant;
  iRow, iCol, iRowCount, iColCount: Integer;
  dStart: TDateTime;
  iSum: Int64;
  v: Variant;
begin
  iSum := 0;
  dStart := Now;
  Write('2. Excel.Application & VarArray: ');
  ovEA := CreateOleObject('Excel.Application');
  try
    ovEA.DisplayAlerts := False; // отключаем сообщения
    ovEA.EnableEvents := False; // отключаем обработку событий
    ovEA.ScreenUpdating := False; // отключаем перерисовку объектов на экране
    ovEA.DisplayStatusBar := False; // отключаем вывод в строку статуса
    ovEA.Workbooks.Open(sFileName);
    iRowCount := ovEA.ActiveSheet.UsedRange.Rows.Count;
    iColCount := ovEA.ActiveSheet.UsedRange.Columns.Count;
    v := VarArrayCreate([1, iRowCount, 1, iColCount], varInteger);
    try
      v := ovEA.Range['A1', Chr($40 + iColCount) + IntToStr(iRowCount)].Value;
      for iRow := 1 to iRowCount do
        for iCol := 1 to iColCount do
          iSum := iSum + v[iRow, iCol];
    finally
      VarClear(v);
    end;
  finally
    ovEA.Quit;
    ovEA := Unassigned;
  end;
  WriteLn(FormatDateTime('nn:ss.zzz', Now - dStart), ' Sum = ', iSum);
end;
Результаты
32-бит: 00:06.326
64-бит: 00:06.241
Чтение диапазона ячеек и присваивание его динамическому вариантному массиву дает приемлемое время...

Вариант 3. Чтение листа MS Excel используя TMS FlexCel
procedure ReadFlexCel;
var
  xls: TXlsFile;
  iRow, iCol, iRowCount, iColCount: Integer;
  dStart: TDateTime;
  eSum: Extended;
begin
  eSum := 0;
  dStart := Now;
  Write('3. FlexCel: ');
  xls := TXlsFile.Create(sFileName);
  try
    xls.IgnoreFormulaText := True;
    iRowCount := xls.RowCount;
    iColCount := xls.ColCount;
    for iRow := 1 to iRowCount do
      for iCol := 1 to iColCount do
        eSum := eSum + xls.GetCellValue(iRow, iCol).AsNumber;
  finally
    xls.Free;
  end;
  WriteLn(FormatDateTime('nn:ss.zzz', Now - dStart), ' Sum = ', Trunc(eSum));
end;
Результаты
32-бит: 00:07.095
64-бит: 00:07.749
Вот это поворот! TMS FlexCel медленее, чем OLE-сервер MS Excel! Такого я не ожидал от библиотеки, читающей файл MS Excel "напрямую" :-( При этом 64-битная версия читает медленнее 32-х битной (это я замечал раньше на своих утилитах). Вот, что написано по этому поводу в документации по FlexCel: "Going 64-bits might not only not improve performance, it might decrease it. In our tests in a 4Gb machine, 64-bit version is consistently slower than the 32-bit one (both versions running in a 64-bit Operating System).". Но я запускал тест на компьютерах, где оперативной памяти не 4ГБ, а 32ГВ! т.е. разработчики, что-то не договариваю и дело не в объеме оперативной памяти. Возможно, дело в вызове ColCount, который пробегает по строкам и находит самую длинную строку? В документации написано "TExcelFile.ColCount is a very slow method in FlexCel, and it is mostly useless". Заменим вызов "iColCount := xls.ColCount" на "iColCount := 5;":
32-бит: 00:06.931
64-бит: 00:07.712
Лучше, но не на много...

Вариант 4. Чтение листа MS Excel используя TMS FlexCel VirtualMode

Но у FlexCel есть еще козырь в рукаве! Это - "Virtual mode". При его использовании, программа не загружает файл целиком в память, а читает ячейки и после прочтения "забывает". Тем самым расход памяти падает, а быстродействие возрастает. В этом режиме не работают многие функции FlexCel, но его можно использовать для программ, которым нужно только прочитать информацию из Excel, т.е. как в нашем случае.
type
  TVirtualCellReader = class
  private
    FSum: Extended;
    procedure OnCellRead(const Sender: TObject;
                         const e: TVirtualCellReadEventArgs);
    function GetSum: Int64;
  public
    constructor Create;
    property Sum: Int64 read GetSum;
  end;

constructor TVirtualCellReader.Create;
begin
  inherited Create;
  FSum := 0;
end;

function TVirtualCellReader.GetSum: Int64;
begin
  Result := Trunc(FSum);
end;

procedure TVirtualCellReader.OnCellRead(const Sender: TObject;
                                        const e: TVirtualCellReadEventArgs);
begin
  FSum := FSum + e.Cell.Value.AsNumber;
end;

procedure ReadFlexCelVirtual;
var
  xls: TXlsFile;
  CellReader: TVirtualCellReader;
  dStart: TDateTime;
  iSum: Int64;
begin
  dStart := Now;
  Write('4. FlexCel VirtualMode: ');
  xls := TXlsFile.Create(True);
  try
    xls.IgnoreFormulaText := True;
    CellReader := TVirtualCellReader.Create;
    try
      xls.VirtualMode := True;
      xls.VirtualCellRead := CellReader.OnCellRead;
      xls.Open(sFileName);
      iSum := CellReader.Sum;
    finally
      CellReader.Free;
    end;
  finally
    xls.Free;
  end;
  WriteLn(FormatDateTime('nn:ss.zzz', Now - dStart), ' Sum = ', iSum);
end;
Результаты
32-бит: 00:06.699
64-бит: 00:07.289
Но и эти результаты далеки от чтения диапазона ячеек листа MS Excel через OLE-сервер. Я разочарован в FlexCel :-(

Вариант 5. Чтение листа MS Excel используя XLSReadWrite
procedure ReadXlsRW;
var
  XLS: TXLSReadWriteII5;
  iRow, iCol, iRowCount, iColCount: Integer;
  dStart: TDateTime;
  eSum: Extended;
begin
  eSum := 0;
  dStart := Now;
  Write('5. XLSReadWrite: ');
  xls := TXLSReadWriteII5.Create(nil);
  try
    xls.FileName := sFileName;
    xls.Read;
    iRowCount := xls[0].LastRow;
    iColCount := xls[0].LastCol;
    for iRow := 0 to iRowCount do
      for iCol := 0 to iColCount do
        eSum := eSum + XLS[0].AsFloat[iCol, iRow];
  finally
    xls.Free;
  end;
  WriteLn(FormatDateTime('nn:ss.zzz', Now - dStart), ' Sum = ', Trunc(eSum));
end;
Результаты
32-бит: 00:05.259
64-бит: 00:05.301
У нас новый лидер! И это библиотека, от которой я отказался лет 7-9 тому назад... и заменил ее на более производительную и функциональную FlexCel...

Вариант 6. Чтение листа MS Excel используя XLSReadWrite DirectRead

За годы моего пользования FlexCel, в XLSReadWrite появился "DirectRead mode", который, по описанию в документации, является аналогом "Virtual mode" из FlexCel.
type
  TXLSReadCell = class
  private
    FSum: Extended;
    procedure OnCellRead(ACell: TXLSEventCell);
    function GetSum: Int64;
  public
    constructor Create;
    property Sum: Int64 read GetSum;
  end;

constructor TXLSReadCell.Create;
begin
  inherited Create;
  FSum := 0;
end;

function TXLSReadCell.GetSum: Int64;
begin
  Result := Trunc(FSum);
end;

procedure TXLSReadCell.OnCellRead(ACell: TXLSEventCell);
begin
  FSum := FSum + ACell.AsFloat;
end;

procedure ReadXlsRWdr;
var
  XLS: TXLSReadWriteII5;
  CellReader: TXLSReadCell;
  dStart: TDateTime;
  iSum: Int64;
begin
  dStart := Now;
  Write('6. XLSReadWrite DirectRead: ');
  xls := TXLSReadWriteII5.Create(nil);
  try
    xls.FileName := sFileName;
    xls.DirectRead := True;
    CellReader := TXLSReadCell.Create;
    try
      xls.OnReadCell := CellReader.OnCellRead;
      xls.Read;
      iSum := CellReader.Sum;
    finally
      CellReader.Free;
    end;
  finally
    xls.Free;
  end;
  WriteLn(FormatDateTime('nn:ss.zzz', Now - dStart), ' Sum = ', iSum);
end;
Результаты
32-бит: 00:04.349
64-бит: 00:04.467
Прирост почти на секунду! "DirectRead mode" в XLSReadWrite, в отличии "Virtual mode" в FlexCel, дает реальный прирост производительности.

Итак, подведем итоги:
1. Axolot XLSReadWrite "DirectRead mode" - 04.349 сек
2. Axolot XLSReadWrite - 05.259 сек
3. MS Excel через OLE-сервер диапазоном ячеек - 06.241 сек
4. TMS FlexCel "Virtual mode" - 06.699 сек
5. TMS FlexCel - 07.095 сек

Таким образом, уверенную победу в чтении информации с листа MS Excel, опережая конкурентов в полтора и более раз, одержала библиотека XLSReadWrite от Axolot Data. Что еще сказать? Умеют шведы делать качественные продукты: Volvo, Ericsson, MySQL, IKEA... и XLSReadWrite...

1 комментарий:

Анонимный комментирует...

Не знаю можно ли адаптировать для Delphi, но для FreePascal есть библиотека http://wiki.lazarus.freepascal.org/FPSpreadsheet
она по скорости интреснее.