C#
Excel
ClosedXML

ClosedXMLでセル結合しすぎるとパフォーマンス劣化する対策

More than 1 year has passed since last update.

ClosedXMLはセル結合しすぎると死ぬ(ほどパフォーマンスが劣化する)

C#でExcelファイルを操作できるライブラリClosedXMLですが、結合セルが多いExcelファイルを扱うと、セル結合(Merge())やファイル保存(SaveAs())のパフォーマンスが格段に落ちます。
その対策です。

バージョン

0.90.0

対策

Merge()に時間がかかる場合
  • Merge()ではなくてMerge(false)を実行すること!
SaveAs()に時間がかかる場合
  • アクセスしているセルのスタイルを変更すること!
上記に共通して
  • 縦1セル横1セルのMerge()を実行していないかチェックすること!

解説

Merge()ではなくてMerge(false)を実行すること! について

Merge()(もしくはMerge(True))は、指定した範囲がセル結合可能かどうかをチェックしてからセル結合します。いわゆる交差(Intersect)チェックです。
Merge(False)では、その交差チェックをせずにセル結合を実行するためパフォーマンスが上がります。
その反面、ありえない(交差チェックに引っかかる)セル結合を行うと壊れたExcelシートが出来上がってしまいます。

アクセスしているセルのスタイルを変更すること! について

セルにアクセスする関数(GetValue()やSetValue()とか)を実行すると、そのアクセスされたセルはClosedXML内部(XLWorkSheet.csのメンバInternals)に保持されます。
その後、ファイルを保存する(SaveAs())際、上記で保持した各セルに対して、空(IsEmpty())かどうかのチェックを行いますがそれに時間がかかっています。
具体的にはXLCell.IsEmpty()の1209行目
if (!Style.Equals(Worksheet.Style) || IsMerged() || HasComment || HasDataValidation)
    return false;

IsMerged()でセルが結合セルかどうかチェックするのに時間がかかっているのですが、それを回避するには!Style.Equals(Worksheet.Style)をfalseにしておけば良いので、例えば
worksheet.Cell(row, col).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
のように実処理に影響が無いスタイルの変更をします。

変更前・後の処理時間は、、、
変更前

        // 無駄にセルアクセスするとIsMerge()されてパフォ劣化する版
        static void Main(string[] args)
        {
            var start = DateTime.Now;
            var workbook = new XLWorkbook();
            var worksheet = workbook.Worksheets.Add("Sample Sheet");
            for (int i = 1; i < 20000; i++)
            {
                worksheet.Range(i, 1, i, 2).Merge(false);
                var temp = worksheet.Cell(i, 1);
                //worksheet.Cell(i, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
                temp = worksheet.Cell(i, 2);
                //worksheet.Cell(i, 2).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
            }
            workbook.SaveAs("HelloWorld.xlsx");
            Console.WriteLine(DateTime.Now - start);
        }
00:01:02.0949905

変更後

        // Style変更によりIsMerge()パフォ劣化が回避される版
        static void Main(string[] args)
        {
            var start = DateTime.Now;
            var workbook = new XLWorkbook();
            var worksheet = workbook.Worksheets.Add("Sample Sheet");
            for (int i = 1; i < 20000; i++)
            {
                worksheet.Range(i, 1, i, 2).Merge(false);
                var temp = worksheet.Cell(i, 1);
                worksheet.Cell(i, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
                temp = worksheet.Cell(i, 2);
                worksheet.Cell(i, 2).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
            }
            workbook.SaveAs("HelloWorld.xlsx");
            Console.WriteLine(DateTime.Now - start);
        }
00:00:03.5360002

こうなります。処理時間が約1/20に削減。

縦1セル横1セルのMerge()を実行していないかチェックすること! について

ClosedXMLでは縦1セル横1セルのMerge()という意味のないセル結合が実行できてしまいます。
例えば

// A1セルからA1セルまでを結合
worksheet.Range(1, 1, 1, 1).Merge(false);

のような実装も特にエラーも出ずに実行でき、通常のセル結合と同様にClosedXML内部にセル結合情報として保持します。なので、このような無用のセル結合情報があればあるほど、IsMerge()やSaveAs()のパフォーマンスが劣化する原因となります。