【问题标题】:Huffman Code Decoder Encoder In Java Source GenerationJava 源代码生成中的 Huffman Code Decoder 编码器
【发布时间】:2015-03-04 09:48:38
【问题描述】:

我想用 Java 创建一个快速的 Huffman 代码解码器,因此想到了查找表。由于这些表会消耗内存,而且我们使用 Java 代码来导航和访问这些表,因此我们可以轻松(或不)编写表达同一张表的程序/方法。

这种方法的问题是,我不知道什么是最好的策略。我知道适合缓存和分支预测的内容很多。此外,switch case 实现意味着实际的 ASM 超出了我的范围。如果我有一个内存查找表(或它的层次结构),我将能够简单地跳进跳出,但出于我的目的,我怀疑该表将适合缓存。

由于我实际上是在走一棵树,因此可以像 else 语句一样实现它,需要一定数量的比较,但对于每个比较,它都需要额外的二元运算。

所以存在以下选项:

  • 在内存查找表中使用的通用算法
  • 决策树的 if/else 表示
  • 使用小型 switch 语句的 if/else 表示来找到正确的符号组(相同的位模式长度)(if 语句越少,代码可能越多)。
  • 代码的switch语句表示

编写和基准测试非常棘手,因此任何初步想法都会很棒。

另一个起作用的问题是位的顺序。最高有效位始终排在第一位,这意味着它以相反的顺序存储。

如果你的树是 A = 0、B = 10、C = 11 来写 BAC,它实际上是 01 + 0 + 11(加号表示追加)。

所以实际上代码必须以相反的顺序编写。对组使用 if /else 或 switch 方法不会有问题,因为屏蔽位很简单,并且位的反转是可能的,但它会失去将组内的索引从掩码中取出的想法,因为相反位顺序添加和删除具有不同的含义,并且无法进行简单的查找。

反转位是一项代价高昂的操作(我使用 4 位查找表),但不会超过二进制操作的性能损失。

但是在移动中反转位更适合这种情况,并且每个位需要四个操作(上移、屏蔽、添加以及下移输入)。由于我提前读取位,所有这些操作都将在寄存器中完成,因此它们可能只需要几个周期。

这样我可以使用 switch、sub 和 if 来找到正确的符号组并返回它们。

所以最后我需要建议。由于我的代码对于语言处理来说是全局的,所以它们可以是硬连线的(即在源代码中)。

我想知道像 ANTRL 这样的解析器生成器是用什么来表达这些决定的。由于它们还根据输入符号进行切换或 if/else,因此可能会给我一个线索。

[更新]

我发现了一种简化方法,可以避免反向位问题,但仍会增加每组的成本。所以我最终按照要遍历的组的顺序写入位。所以我不需要每个位进行四次修改,而是每个组(不同的位长度)。

对于每个组,我们有: 1. 第一个元素的值,大小(以及该组中最后一个元素的值。

因此,对于每个组,算法如下所示: 1.读取mbits并结合当前读取的值。 2.将该值与该组的最后一个值进行比较,如果不是其外部,则该值是否在该组内较小。 -> 阅读下一个 3. 如果它在组内,则可以访问值数组或使用 switch 语句。

这是完全通用的,可以在没有循环的情况下使用,从而提高效率。此外,如果检测到该组,则代码的位长是已知的,并且可以从源中消耗这些位,因为代码向前看很远(从流中读取)。

[更新 2]

要访问实际值,可以使用按组分组的单个大元素数组。由于组与组之间的概率降低,很可能很大一部分适合 L2 或 L1 缓存,从而加快此处的访问速度。

或者使用 switch 语句。

[更新 3]

根据开关的情况,编译器会生成一个表开关或一个查找开关。查找开关的复杂度为 O(log n) 并存储 key、jmp 偏移对,这是不可取的。因此检查组更适合 if/else。

tableswitch 本身只使用一个跳转偏移量表,它只需要减去、比较、访问、jmp 即可到达目的地,而不是必须在常量上执行返回值。

因此,表访问看起来更有希望。此外,为了避免不必要的跳转,每个组可能包含访问和返回组符号表的逻辑。将所有内容存储在大表中是有希望的,因为它可能是 int 或每个符号的短,而我的代码通常最多只有 1000 到 4000 个符号,使其实际上很短。

我将检查 1 - 模式是否会让我有机会以更好的方式存储和访问掩码,从而允许二进制搜索正确的组而不是在 O(n) 中前进,甚至可能在期间完全避免任何移位操作处理。

【问题讨论】:

  • 一个 switch 可能会导致一系列测试和分支操作,因此可能没有级联 if 语句的优势。
  • 我不太明白您对“位顺序”和以下段落的意思。如果您在一个字节中有(使用您的 ABC 代码)01011xxx,它可能被解码为 ABC 或 CAB,而不是 BAC。
  • ANTLR 将语法编译成 if 语句,测试前面词法分析器产生的标记类别,其中字符根据正则表达式累积。
  • 您确定 ANTLR 吗?我认为它也适用于 switch 语句。
  • github.com/linkedin/bowser/blob/master/bowser-core/src/main/… 看看他们使用 switch 语句,但我不知道为什么。

标签: java performance parsing huffman-code


【解决方案1】:

我无法理解您在(长)问题中写的大部分内容,但有一个简单的方法。

我们将从一张桌子开始。假设您最长的霍夫曼码是 15 位。 (事实上​​,deflate 将其 Huffman 代码的大小限制为 15 位。)然后构建一个包含 32768 个条目的表,其中每个条目是下一个代码中的位数,以及该代码的符号。对于少于 15 位的代码,表中有多个相同代码的条目。例如。如果符号“C”的代码是 10010110(7 位),则表 xxxxxxxx10010110 的所有索引都具有相同的内容。这些条目都有 {7, 'C'}。

然后您从流中获得 15 位,并在表中查找下一个代码。您从该表条目中删除位数,并使用生成的符号。现在您可以从流中获得所需的 15 位,然后重复。因此,如果您使用了 7 位,则再获得 8 位以返回 15 并查找下一个代码。

下一个微妙之处是,如果您的 Huffman 代码经常更改,您最终可能会花费更多时间来为每个新的 Huffman 代码填充该大表,而不是实际解码所花费的时间。为避免这种情况,您可以制作一个两级表,例如,对代码的第一部分进行 9 位查找(512 个条目)。如果代码为 9 位或更少,则按上述方法进行。这将是最常见的情况,因为较短的代码更频繁(这是霍夫曼编码的重点)。如果表条目表明代码中有 10 位或更多位(并且您还不知道还有多少位),那么您将使用前 9 位并转到二级表中指向的最初 9 位通过第一个表中的条目,该表具有剩余六位(64 个条目)的条目。这解决了代码的其余部分,因此告诉您要消耗多少位以及符号是什么。这种方法可以大大减少填写表格所花费的时间,并且几乎与短代码一样快,因为短代码更常见。这是inflatezlib中使用的方法。

【讨论】:

  • 我的目标是速度和内存紧凑。我想对客户端(Java、JavaScript)的解码/编码使用相同的方法。代码表也有点复杂。事实上,我混合了三个不同的代码和一个字典,以获得一个平均基于 .所以最后我的目标是静态解码器(编码器更原始并且不经常使用,因为服务器上的场景大多是只读的)。所以编码器已经完成了。大型查找表是不可取的,而且是遥不可及的,因为它们会降低性能。
  • 在当前版本中包含代码中的所有内容和所有更新的优化,我每秒可以解压缩大约 1 亿个符号,其中 50% 的符号将是完整的单词。压缩在 1:3 到 1:4 之间,每个字符占用 16 位,它的 1:6 到 1:8,所以它是值得的。由于字符串的代码字是稳定的,因此它们甚至可以很好地用作键。
  • @MartinKersten:我认为 Mark 的方法非常好。您可以通过决定每次咀嚼多少位来控制表格大小,并根据需要使用辅助表格来处理余额。如果 15 多于 Mark 建议的 9 或您认为最好的任何数字。
  • 顺便说一句,这不是一个长问题,它只是对更复杂语句的更长描述。当你写信时不明白我写的大部分内容,我想我失败了。
  • 我已经有了用于动态代码的 8 位大小的查找表。与源代码的当前优化表示相比,与查找相比,使用源代码的速度快四倍。而且它根本不共享任何结构,因为我内联并且数组访问是只读的,我每秒管理大约 3.5 亿个符号,四个线程用于 256 个符号 9 组代码。不要问查找表方法执行了什么,即使它只访问一个主表(除了 12 位)。它慢了大约 10 倍。
【解决方案2】:

最后还是很简单的。我现在支持几乎所有的解决方案。可以测试每个符号组(相同的位长),使用查找表(10bit + 10bit + 10bit(只是 10bit 的表,symbolcount + 1 是对这些 talbes 的引用))并生成 java(如果需要 javascript,但目前我使用 GWT 进行翻译)。

我什至使用长读和移位操作来减少对二进制信息的访问。这样代码会变得更高效,因为我只支持最大位大小(20 位(即表格中的表格),它产生 2^20 个符号,因此最多为一百万)。

对于排序,我只使用移位操作来生成位掩码,不需要反转位顺序等。

表查找也可以用 Java 表示,将表存储为数组的数组(有趣的是,java 文件可以有多大,而没有编译器抱怨)。

我还发现有趣的是,由于比较是表示排序(我猜是半阶),因此可以对符号进行排序,而不是将符号映射到比较索引。通过比较两个索引,可以简单地对代码流进行排序,而无需过多接触。通过同时存储前两个比较索引(16 位或 32 位),可以使用相同的 Huffman 代码有效地对压缩字符串进行排序,从而对压缩字符串进行二进制排序,这使得压缩某种语言的字符串非常理想。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-08-20
    • 2011-04-17
    • 2015-12-13
    • 2011-02-25
    • 1970-01-01
    • 2011-10-23
    • 1970-01-01
    • 2012-04-21
    相关资源
    最近更新 更多