Excel库调研

库名 测试用例 效率
ExcelDataReader 250excel文件,共计50w行 16000 ms
NPOI 250excel文件,共计50w行 60000 ms
EPPlus 250excel文件,共计50w行 60000 ms

ExcelDataReader优势是可以按需读取,内存占用、读取效率都要比另外两个快,NPOI、EPPlus貌似只能全量读取

使用yield return

测试发现 yield return 返回IEnumerable对象,读取Excel的IO效率会提高很多,从16000ms,到7000ms

以下代码,V1版本要比V2版本快将很多。

public static IEnumerable<ExcelData> LoadExcel_V1(string filePath)
{
    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        using (var reader = ExcelReaderFactory.CreateReader(stream))
        {
            var counter = 0;
            do
            {
                if (IsSheetNameValid(reader.Name))
                {
                    counter++;
                    ExcelData data;
                    try
                    {
                        Console.WriteLine($"parsing...... {reader.Name} ({counter}/{reader.ResultsCount})");
                        data = ParseExcel(reader);
                    }
                    catch (Exception e)
                    {
                        throw new Exception($"excel:{filePath} sheet:{reader.Name} 读取失败.", e);
                    }
                    if (data != null)
                    {
                        yield return data;
                    }
                }
            }
            while (reader.NextResult());
        }
    }
}

public static IEnumerable<ExcelData> LoadExcel_V2(string filePath)
{
    var ret = new List<ExcelData>();
    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        using (var reader = ExcelReaderFactory.CreateReader(stream))
        {
            var counter = 0;
            do
            {
                if (IsSheetNameValid(reader.Name))
                {
                    counter++;
                    ExcelData data;
                    try
                    {
                        Console.WriteLine($"parsing...... {reader.Name} ({counter}/{reader.ResultsCount})");
                        data = ParseExcel(reader);
                    }
                    catch (Exception e)
                    {
                        throw new Exception($"excel:{filePath} sheet:{reader.Name} 读取失败.", e);
                    }
                    if (data != null)
                    {
                        ret.Add(data);
                    }
                }
            }
            while (reader.NextResult());
        }
    }

    return ret;
}

yield return 坑点

本读表案例,多了一次forloop遍历,会多增加3000ms读表、解析表格耗时。7000ms上升到10000ms。

坑点可看以下代码

private static void TestYield()
{
    IEnumerable<string> list()
    {
        yield return "111";
        Console.WriteLine("work");
        yield return "222";
    }

    var a = list();

    foreach (var item in a)
    {
        Console.WriteLine("outside loop: " + item);
    }

    Process(a);
}

private static void Process(IEnumerable<string> inData)
{
    foreach (var item in inData)
    {
        Console.WriteLine("Process: " + item);
    }
}

// result:
// outside loop: 111
// work
// outside loop: 222

// Process: 111
// work
// Process: 222

// 每次forloop,work都会被调用,需要尽量避免,特别是方法里有繁重逻辑

使用Span

Span已是 .Net Standard 2.1的标准库的一部分,2.0 需要引入nuget才行。

Span(ref struct)为内存切片,是内存片段的封装,可以接住栈内存托管堆非托管堆内存进行操作,但span有一些限制,不能作为成员变量,不能在async方法里使用,这些情况,有另一个类型Memory来处理。以前版本,也可以开启unsafe直接使用指针操作,但是很危险,所以.net为了性能提供了span/memory,同时保证了代码安全。

Span能有效降低字符串SubString等操作的开销,不会生成新的字符串,减少gc。

但是Span对字符串操作,还没有提供Split方法,这个比较蛋疼。
使用这个ReadOnlySpan.Split,性能比String.Split快一丢丢,也没有零时字符串生成了,但使用起来繁琐一些

MGF后续升级 .Net Standard 2.1后,也将会大量使用Span

多key的实现

支持1到4个key作为数据行的索引,每个key(int),但支持值得范围有限,最终合成唯一combinekey(ulong),支持负数。

ulong为64bit。2个key,则int32和int32,合成64bit,3个key则,int32,int16,int16合成64bit。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong GetKey(int key1, int key2)
{
    return (((ulong)key1 & 0xffffffff) | (((ulong)key2 & 0xffffffff) << 32));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong GetKey(int key1, int key2, int key3)
{
    short shortKey2 = System.Convert.ToInt16(key2);
    short shortKey3 = System.Convert.ToInt16(key3);
    return (((ulong)key1 & 0xffffffff) | (((ulong)shortKey2 & 0xffff) << 32) | (((ulong)shortKey3 & 0xffff) << 48));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong GetKey(int key1, int key2, int key3, int key4)
{
    short shortKey1 = System.Convert.ToInt16(key1);
    short shortKey2 = System.Convert.ToInt16(key2);
    short shortKey3 = System.Convert.ToInt16(key3);
    short shortKey4 = System.Convert.ToInt16(key4);
    return (((ulong)shortKey1 & 0xffff) | (((ulong)shortKey2 & 0xffff) << 16) | (((ulong)shortKey3 & 0xffff) << 32) | (((ulong)shortKey4 & 0xffff) << 48));
}

代码生成

使用CodeDom,部分代码片使用StringBuilder拼接。

Github

https://github.com/Sarofc/GTable