【问题标题】:Java: multithreaded character stream decodingJava:多线程字符流解码
【发布时间】:2010-08-09 08:39:10
【问题描述】:

我正在维护一个高性能 CSV 解析器,并尝试充分利用最新技术来提高吞吐量。对于这个特定的任务,这意味着:

  • 闪存(我们拥有相对便宜的 PCI-Express 卡,1 TB 的存储空间,可达到 1 GB/s 的持续读取性能)
  • 多核(我们拥有一台具有 16 个硬件线程的廉价 Nehalem 服务器)

CSV 解析器的第一个实现是单线程的。文件读取、字符解码、字段拆分、文本解析,都在同一个线程中。结果是大约 50MB/s 的吞吐量。不错,但远低于存储限制...

第二种实现使用一个线程读取文件(在字节级别),一个线程解码字符(从ByteBuffer到CharBuffer),以及多个线程解析字段(我的意思是将分隔的文本字段解析为双精度,整数,日期...)。这在我们的机器上运行得更快,接近 400MB/s。

但仍远低于我们存储的性能。那些 SSD 将来会再次改进,我们并没有在 Java 中充分利用它。很明显,当前的限制是字符解码( CharsetDecoder.read(...) )。这就是瓶颈,在强大的 Nehalem 处理器上,它以 400MB/s 的速度将字节转换为字符,非常好,但这必须是单线程的。 CharsetDecoder 在某种程度上是有状态的,取决于使用的字符集,并且不支持多线程解码。

所以我向社区提出的问题是(感谢您到目前为止阅读这篇文章):有人知道如何在 Java 中并行化字符集解码操作吗?

【问题讨论】:

    标签: java multithreading character-encoding


    【解决方案1】:

    有谁知道如何在 Java 中并行化字符集解码操作?

    您也许可以打开多个输入流来执行此操作(我不确定您将如何使用 NIO 进行此操作,但它必须是可能的)。

    这有多困难取决于您要解码的编码。您将需要针对目标编码的定制解决方案。如果编码具有固定宽度(例如 Windows-1252),则 1 个字节 == 1 个字符,解码很容易。

    现代可变宽度编码(如 UTF-8 和 UTF-16)包含用于识别字符序列的第一个字节的规则,因此可以跳转到文件中间并开始解码(您将拥有注意前一个块的结尾,所以最好先解码文件的结尾)。

    一些传统的可变宽度编码可能没有设计得这么好,因此您别无选择,只能从数据的开头解码并按顺序读取。

    如果可以选择,请将您的数据生成为 UTF-16BE。然后你可以去掉解码,直接读取两个字节到一个字符。

    如果文件是 Unicode,请注意 BOM 处理,但我猜您已经熟悉许多低级细节。

    【讨论】:

    • 很遗憾,UTF-16 是一种可变长度编码。如此简单的 Unicode 解析需要 UTF-32。
    • @grddev - 我在帖子中介绍了这一点 - 可以识别 UTF-16 数据流中间的字符序列 - 高代理项对是 0xD800-0xDBFF,低代理项是 0xDC00-0xDFFF。其他任何内容都包含在一对字节中。
    • 我的评论提到了 UTF-16BE。你不能完全切断解码。但这真的很简单。
    • 感谢那些鼓舞人心的元素。当然,固定长度的编码相对容易并行化。您可以将原始字节存储在块中,确保块不会在中间切割字符,在并发任务中解码块并组装结果。当我检测到可变长度的字符集时,也许我应该这样做并恢复到单线程模式。当然,我的目标是提供一个 CSV 解析器,它适用于任何字符集,向世界各地的客户提供其奇特的字符集。
    • 也许可以在 jdk 中获取各种 CharsetDecoder 的后继者(如 sun.nio.cs.UTF_8.Decoder 等)的源代码,看看如何并行化它们?
    【解决方案2】:

    很明显,当前的限制是字符解码( CharsetDecoder.read(...) )

    你怎么知道的?您的监控/分析是否确凿地显示解码器线程正在使用您的一个内核的 100%?

    另一种可能是操作系统无法以理论最高速度驱动 SSD。

    如果 UTF-8 解码绝对是瓶颈,那么应该可以并行执行任务。但您肯定需要实现自己的解码器才能做到这一点。

    【讨论】:

    • 是的,使用 JProfiler 的几次运行清楚地表明(单个)字符解码线程几乎 100% 的时间都处于活动状态。我在响应中看到了 UTF-8 和 UTF-16 编码的多个引用。但是我们在这里编写了一个通用的 CSV 解析器,它将被我们在欧洲、美国、日本、中国的客户用于现有文件......所以我们不能假设将使用什么字符集。特别是我们不能假设字符集是否是固定长度的。
    【解决方案3】:

    另一种(疯狂的)替代方法是将输入分成任意大小的块,忽略解码问题,然后并行解码每个块。但是,您希望确保块重叠(具有参数化大小)。如果两个块的重叠区域被两个线程以相同的方式解码(并且您的重叠对于指定的编码来说足够大),那么加入结果应该是安全的。重叠越大,需要的处理越多,出错的概率越小。此外,如果您知道编码是 UTF-8 或类似的简单编码,则可以将重叠设置得非常低(对于该客户端),并且仍然可以保证正确操作。

    如果第二个块被证明是错误的,你将不得不重做它,所以不要对大块进行并行处理是很重要的。如果您并行执行两个以上的块,则从头到尾“修复”很重要,这样一个未对齐的块不会导致下一个块(可能正确对齐)无效。

    【讨论】:

      【解决方案4】:

      如果您知道编码,并且它是固定大小的,或者不包含重叠的字节序列,则可以扫描特殊序列。在 CSV 中,换行符的顺序可能有意义。即使您动态检测编码,您也可以运行前几个字节以确定编码,然后继续进行并行解码。

      【讨论】:

      • 我非常喜欢这个想法。直接在原始字节中定位分隔符。是的,NEW_LINE 模式是 CSV 解析器的正确候选者。但我必须支持任何字符集。您是否知道有关字符集实现的一些通用方法可以判断字节模式是否重叠?我在 Javadoc 中没有看到任何内容。
      • @Antoine:不幸的是,我不知道。在任何 UTF 编码或任何固定宽度编码中,这都不应该是一个问题。 According to this question 典型的日语编码也应该没有问题。我不知道任何换行符表示在中文(或其他)编码中是否重叠。无论如何,很明显,Java 中的现有接口并没有提供很好地做到这一点的方法。 :(
      猜你喜欢
      • 1970-01-01
      • 2013-12-14
      • 1970-01-01
      • 2021-06-05
      • 2016-06-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多