我正在使用 C# 开发一个名为 MimeKit 的 MIME 和 mbox 解析器。
它基于我编写的早期 MIME 和 mbox 解析器(例如 GMime),速度非常快(可以在大约 1 秒内解析 1.2GB mbox 文件中的每条消息)。
我还没有测试 MimeKit 的性能,但我在 C# 中使用了许多我在 C 中使用的相同技术。我怀疑它会比我的 C 实现慢,但因为瓶颈是 I/O并且 MimeKit 是为像 GMime 一样进行最佳(4k)读取而编写的,它们应该非常接近。
您发现当前方法运行缓慢(StreamReader.ReadLine(),合并文本,然后将其传递给 SharpMimeTools)的原因如下:
-
StreamReader.ReadLine() 不是从文件中读取数据的最佳方式。虽然我确定 StreamReader() 会进行内部缓冲,但它需要执行以下步骤:
A) 将从文件中读取的字节块转换为 unicode(这需要遍历从磁盘读取的 byte[] 中的字节,以将从流中读取的字节转换为 unicode char[])。
B) 然后它需要遍历其内部的 char[],将每个 char 复制到 StringBuilder 中,直到找到一个 '\n'。
因此,只需读取行,您就至少有 2 次通过您的 mbox 输入流。更不用说正在进行的所有内存分配......
-
然后你将你读过的所有行组合成一个单一的巨型字符串。这需要再次传递您的输入(大概将从 ReadLine() 读取的每个字符串中的每个字符复制到 StringBuilder 中?)。
我们现在对输入文本进行了多达 3 次迭代,甚至还没有进行任何解析。
1234563在解析任何内容之前进行 5 次迭代。如果 SharpMimeMessageStream 发现它读得太远,它还有一种方法可以“撤消” ReadLine()。因此可以合理地假设他至少扫描了 一些 行。更不用说正在进行的所有字符串分配......呃。
对于每个标题,一旦 SharpMimeTools 拥有其行缓冲区,它就会拆分为字段和值。那是另一个关卡。到目前为止,我们最多可以通过 6 次。
SharpMimeTools 然后使用 string.Split()(这很好地表明此 mime 解析器不符合标准)通过拆分 ',' 和参数化标头(例如 Content-Type 和Content-Disposition) 通过拆分';'。那是另一个关卡。 (我们现在最多 7 次。)
一旦拆分它们,它将对从 string.Split() 返回的每个字符串运行正则表达式匹配,然后每个 rfc2047 编码字令牌传递更多正则表达式,然后最终对编码字字符集和有效负载进行另一次传递成分。到目前为止,我们所说的大部分输入至少经过 9 或 10 次传递。
我放弃了进一步的考试,因为它的通过次数已经是 GMime 和 MimeKit 所需的 2 倍多,而且我知道我的解析器可以优化为至少比它们少 1 次通过做。
另外,作为旁注,任何解析字符串而不是 byte[](或 sbyte[])的 MIME 解析器永远不会很好。电子邮件的问题在于,如此多的邮件客户端/脚本/等会在标题和消息正文中发送未声明的 8 位文本。 unicode 字符串解析器可能如何处理呢?提示:不能。
using (var stream = File.OpenRead ("Inbox.mbox")) {
var parser = new MimeParser (stream, MimeFormat.Mbox);
while (!parser.IsEndOfStream) {
var message = parser.ParseMessage ();
// At this point, you can do whatever you want with the message.
// As an example, you could save it to a separate file based on
// the message subject:
message.WriteTo (message.Subject + ".eml");
// You also have the ability to get access to the mbox marker:
var marker = parser.MboxMarker;
// You can also get the exact byte offset in the stream where the
// mbox marker was found:
var offset = parser.MboxMarkerOffset;
}
}
2013 年 9 月 18 日更新: 我已将 MimeKit 升级到现在可用于解析 mbox 文件并成功解决问题的地步,但速度并没有那么快作为我的 C 库。这是在 iMac 上测试的,因此 I/O 性能不如我的旧 Linux 机器(GMime 能够在大约 1 秒内解析类似大小的 mbox 文件):
[fejj@localhost MimeKit]$ mono ./mbox-parser.exe larger.mbox
Parsed 14896 messages in 6.16 seconds.
[fejj@localhost MimeKit]$ ./gmime-mbox-parser larger.mbox
Parsed 14896 messages in 3.78 seconds.
[fejj@localhost MimeKit]$ ls -l larger.mbox
-rw-r--r-- 1 fejj staff 1032555628 Sep 18 12:43 larger.mbox
如您所见,GMime 仍然要快一些,但我对如何提高 MimeKit 解析器的性能有一些想法。事实证明,C# 的fixed 语句非常昂贵,所以我需要重新设计它们的用法。例如,a simple optimization 我昨天做了大约 2-3 秒的总时间(如果我没记错的话)。
优化更新:通过替换将性能提高了 20%:
while (*inptr != (byte) '\n')
inptr++;
与:
do {
mask = *dword++ ^ 0x0A0A0A0A;
mask = ((mask - 0x01010101) & (~mask & 0x80808080));
} while (mask == 0);
inptr = (byte*) (dword - 1);
while (*inptr != (byte) '\n')
inptr++;
优化更新:通过放弃使用 Enum.HasFlag() 并改用直接位掩码,我终于能够使 MimeKit 与 GMime 一样快。
MimeKit 现在可以在 3.78 秒内解析相同的 mbox 流。
作为比较,SharpMimeTools 需要 20 多分钟(为了测试这一点,我不得不将电子邮件分成单独的文件,因为 SharpMimeTools 无法解析 mbox 文件)。
另一个更新:我已经通过整个代码中的各种其他调整将它降低到 3.00 秒。