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拼接。