事情是这个样子的
在QQ群在聊把如何100万数据导出成CSV文件?会不会很慢?
俺回了一句“现在的机器性能好,没啥问题”。
然后大家开始谈论机器的配置了。哎,俺的机器配置有点差。
然后俺就进行了一个测试。
测试数据
数据定义
public struct Rec
{
public int v1;
public int v2;
}
private Rec[] Data;
这里为啥不用class呢,因为这个场景struct 更简单。
测试数据生成
Data = new Rec[200*10000];
Random r = new Random(DateTime.Now.Millisecond);
for(int i= 0;i < Data.Length;i++)
{
Data[i].v1 = r.Next();
Data[i].v2 = r.Next();
}
测试数据200万行,每行2个数字。
测试结果
测试程序为 .NET Framework 4.8 64 Release 版本
如果用.Net 8 版本进行测试,除了方法3之外,会快一些。方法3速度基本不变。
开始写代码
方法一
这是最最普通的生成CSV的方法,就是一个一个数据的写入到CSV中。没啥可说的。就是用来当做参照物的,耗时0.536秒。
string fn = @"1.csv";
FileStream fs = new FileStream(fn, FileMode.Create);
DateTime t1 = DateTime.Now;
byte[] buff;
byte[] buff_rn = ASCIIEncoding.ASCII.GetBytes("\r\n");
byte[] buff_split = ASCIIEncoding.ASCII.GetBytes(",");
string s ="v1,v2";
buff = ASCIIEncoding.ASCII.GetBytes(s);
fs.Write(buff,0,buff.Length);
foreach (Rec i in Data)
{
fs.Write(buff_rn, 0, buff_rn.Length);
s = i.v1.ToString();
buff = ASCIIEncoding.ASCII.GetBytes(s);
fs.Write(buff, 0, buff.Length);
fs.Write(buff_split, 0, buff_split.Length);
s = i.v2.ToString();
buff = ASCIIEncoding.ASCII.GetBytes(s);
fs.Write(buff, 0, buff.Length);
}
fs.Flush();
fs.Close();
fs.Dispose();
DateTime t2 = DateTime.Now;
label1.Text=("输出"+Data.Length.ToString()+"行CSV文件,耗时" +((t2 - t1).TotalMilliseconds/1000).ToString("0.000")+"秒");
方法二
在方法1上加了一个8M的缓存。其实这种写法在其他语言(c++、delphi 等)还有效果,C#效果不大,因为C#的文件流本身有优惠。耗时0.503秒。
string fn = @"2.csv";
int ms_size = 8 * 1024 * 1024;
byte[] ms=new byte[ms_size + 1024];
int ms_position = 0;
FileStream fs = new FileStream(fn, FileMode.Create);
DateTime t1 = DateTime.Now;
byte[] buff;
byte[] buff_rn = ASCIIEncoding.ASCII.GetBytes("\r\n");
byte[] buff_split = ASCIIEncoding.ASCII.GetBytes(",");
string s = "v1,v2";
buff = ASCIIEncoding.ASCII.GetBytes(s);
Array.Copy(buff, 0, ms, ms_position, buff.Length);
ms_position+= buff.Length;
foreach (Rec i in Data)
{
Array.Copy(buff_rn, 0, ms, ms_position, buff_rn.Length);
ms_position += buff_rn.Length;
s = i.v1.ToString();
buff = ASCIIEncoding.ASCII.GetBytes(s);
Array.Copy(buff, 0, ms, ms_position, buff.Length);
ms_position += buff.Length;
Array.Copy(buff_split, 0, ms, ms_position, buff_split.Length);
ms_position += buff_split.Length;
s = i.v2.ToString();
buff = ASCIIEncoding.ASCII.GetBytes(s);
Array.Copy(buff, 0, ms, ms_position, buff.Length);
ms_position += buff.Length;
if (ms_position > ms_size)
{
fs.Write(ms,0, ms_position);
ms_position = 0;
}
}
if (ms_position > 0)
{
fs.Write(ms, 0, ms_position);
}
fs.Flush();
fs.Close();
fs.Dispose();
DateTime t2 = DateTime.Now;
label2.Text = ("输出" + Data.Length.ToString() + "行CSV文件,耗时" + ((t2 - t1).TotalMilliseconds / 1000).ToString("0.000") + "秒");
优化
我们先分析一下,生成CSV中,主要的计算是啥?在这个测试上主要计算就是,整型转字符串。执行了400万次(200万行,每行2个数字)整型转字符串。那么我们就先测试一下执行了400万次整型转字符串多少时间。
string s = "";
DateTime t1 = DateTime.Now;
foreach (Rec i in Data)
{
s = i.v1.ToString();
s = i.v2.ToString();
}
DateTime t2 = DateTime.Now;
label4.Text = ( (Data.Length*2).ToString() + "次ToString,耗时" + ((t2 - t1).TotalMilliseconds / 1000).ToString("0.000") + "秒");
耗时是0.265秒
那么我们就像从 整型转字符串 开始优化。
方法三
使用了 IntToString 函数,速度优化到了 耗时0.176秒。优化到这里出现了一个有趣的数据:
只执行 整型转字符串 耗时是0.265秒,方法三的速度比只 整型转字符串 还快。为什么呢?
因为在 方法三 中,其实没有使用字符串,取消中间商赚差价。
string fn = @"3.csv";
int ms_size = 8 * 1024 * 1024;
byte[] ms = new byte[ms_size + 1024];
int ms_position = 0;
FileStream fs = new FileStream(fn, FileMode.Create);
DateTime t1 = DateTime.Now;
byte[] buff;
byte[] buff_rn = ASCIIEncoding.ASCII.GetBytes("\r\n");
byte[] buff_split = ASCIIEncoding.ASCII.GetBytes(",");
string s = "v1,v2";
buff = ASCIIEncoding.ASCII.GetBytes(s);
Array.Copy(buff, 0, ms, ms_position, buff.Length);
ms_position += buff.Length;
foreach (Rec i in Data)
{
Array.Copy(buff_rn, 0, ms, ms_position, buff_rn.Length);
ms_position += buff_rn.Length;
IntToString(i.v1,ref ms, ref ms_position);
Array.Copy(buff_split, 0, ms, ms_position, buff_split.Length);
ms_position += buff_split.Length;
IntToString(i.v2, ref ms, ref ms_position);
if (ms_position > ms_size)
{
fs.Write(ms, 0, ms_position);
ms_position = 0;
}
}
if (ms_position > 0)
{
fs.Write(ms, 0, ms_position);
}
fs.Flush();
fs.Close();
fs.Dispose();
DateTime t2 = DateTime.Now;
label3.Text = ("输出" + Data.Length.ToString() + "行CSV文件,耗时" + ((t2 - t1).TotalMilliseconds / 1000).ToString("0.000") + "秒");
这个代码的功能 其实就个 int.ToString。 看上去下面这个代码有点拉胯,像是那种故意搞笑的代码,是的确实拉胯,但是它确实快。
public static void IntToString(int n,ref byte[] buff,ref int position)
{
int len = 0;
int len_inc = 16;
int p = position+ len_inc;
do
{
buff[p] = (byte)((n % 10)+48);
p += 1;
len += 1;
}
while ((n /= 10) != 0);
p -= 1;
for (int i = 0; i < len;i++)
{
buff[position]= buff[p];
position += 1;
p -= 1;
}
}
其他
Release 版本 和 Debug版本差别很大,例如 方法三 的Debug版 速度并不快,但是用Release 版本,方法3就明显快。
使用.Net 8 版本进行测试,结果如下
.NET Framework 4.8 | .Net 8 | 对比 | |
---|---|---|---|
方法1 | 0.538 | 0.257 | .Net 8 快很多 |
方法2 | 0.503 | 0.190 | .Net 8 快很多 |
方法3 | 0.176 | 0.178 | 两者差不多 |
整型转字符串 | 0.265 | 0.075 | .Net 8 快很多 |
上面的数据进行时测试,不是很准确。但是可以看出 :
.Net 8 在 整型转字符串 上要快很多。方法三因为没有用 .ToString() 所以 速度在.NET Framework 4.8 和 .Net 8 下差不多。