【问题标题】:The most efficient way to deal with bits for compression in JavaScript在 JavaScript 中处理压缩比特的最有效方法
【发布时间】:2019-07-06 19:23:51
【问题描述】:

我从未做过压缩,但对Huffman encoding 很感兴趣。他们将其显示为前几个字母的简单演示编码:

A     0
E     10
P     110
space 1110
D     11110
T     111110
L     111111

您看到的标准 Huffman 编码具有不同的代码集,但这对于这个问题并不重要。我想知道的是如何最有效地在 JavaScript 中操作这些位。据说您应该以 8、16 或 32 个块的形式处理事物,但实际上仅此而已,因为这就是整数和值在计算机体系结构中的存储方式。所以我理解的方式是你可能应该一次读取 8 位的输入块。我不确定该怎么做,但我认为如果你这样做,它会起作用:

var bytes = new Uint8Array(array)
var byte1 = bytes[0]
var byte2 = bytes[1]
...

这似乎是访问数据的最有效方式。但是我正在考虑另一种选择,我想澄清一下。您可以改为将输入转换为二进制文本字符串,即 1 和 0 的字符串,如

var string = integerOrByteArray.toString(2)

但我了解到,将任何内容转换为字符串都会影响性能。因此,您似乎应该尽可能避免转换为字符串。

因此,如果是这种情况,那么我们将使用Uint8Array(或Uint32Array 等)的第一种方法。我想知道如何将值有效/理想地拆分为组件部分。所以如果我们有这个......

010110
AEP

....我们做了整数的事情,然后我们可能会加载一些 8 位整数,比如其中之一:

01011000
01011001
00101100
...

就像是,我们需要(可能)加入任何可能是最后一个 8 位块的一部分的前端数据,然后将剩余的数据拆分为字符。我的问题基本上是推荐的这样做方式。我可以想出一些方法,但到目前为止它们看起来都相当复杂。

【问题讨论】:

  • TypedArrays 有 .slice().subarray() 方法。
  • @guest271314 仅适用于字节粒度。

标签: javascript encoding binary bit-manipulation huffman-code


【解决方案1】:

这实际上与霍夫曼减压的“休息”相互作用。您究竟需要什么取决于您是打算进行有效的基于表的解码还是逐位树遍历。输入不能不解码就被拆分,因为你只有在解码它代表什么符号后才能找到代码的长度。解码后,拆分的意义不大,所以我们最终得到的只是一个霍夫曼解码器,而不是一个位串拆分器。

对于逐位树遍历,您只需要某种方式来访问字节数组中的任何特定位(给定其索引)。您也可以使用以下技术,块大小为 1 位。

为了更高效的解码,您需要一个缓冲区,您可以从中提取一个位块,只要您预定义的最大代码长度(例如 15 位左右)[1]。具体取决于您的代码被打包成字节的顺序,即字节是填充 lsb-to-msb 还是 msb-to-lsb,以及您希望在缓冲区变量中保留这些位的位置。例如,这里我将缓冲区中的位保留在缓冲区的 lsb 附近,并假设如果代码被分成两个字节,那么它在第一个字节的 lsb 和第二个字节的 msb[ 2](未测试):

var rawindex = 0;
var buffer = 0;
var nbits = 0;
var done = false;
var blockmask = (1 << MAX_CODELEN) - 1;
while (!done) {
    // refill buffer
    while (nbits < MAX_CODELEN && rawindex < data.length) {
        buffer = (buffer << 8) | data[rawindex++];
        nbits += 8;
    }
    if (nbits < MAX_CODELEN) {
        // this can happen at the end of the data
        buffer <<= MAX_CODELEN - nbits;
        nbits = MAX_CODELEN;
    }
    // get block from buffer
    var block = (buffer >> (nbits - MAX_CODELEN)) & blockmask;
    // decode by table lookup
    var sym = table[block];
    // drop only bits that really belong to the symbol
    nbits -= bitlengthOf(sym);
    ...
    // use the symbol somehow
}

这显示了最简单的基于表的解码策略,只是简单的查找。符号/长度对可以是一个对象,也可以存储在两个单独的 Uint8Array 中,或者编码为一个 Uint16Array,诸如此类。建表很简单,例如用伪代码:

# for each symbol/code do this:
bottomSize = maxCodeLen - codeLen
topBits = code << bottomSize
for bottom in inclusive_range(0, (1 << bottomSize) - 1):
    table[topBits | bottom] = (symbol, codeLen)

变体

将代码从 lsb up 打包成字节会改变比特流。要重新组装缓冲区中的代码,位需要从缓冲区的高端进入并从底部离开:

// refill step
buffer |= data[rawindex++] << nbits;
nbits += 8;
...
// get block to decode
var block = buffer & blockmask;
// decode by table lookup
var sym = table[block];
// drop bits
buffer >>= getlengthOf(sym);

表格也不同,现在填充位于表格索引的高位,分散属于单个符号的条目而不是将它们放在连续范围内(未经测试,显示位压缩的表格条目5位码长):

// for each symbol/code:
var paddingCount = MAX_CODELEN - codeLen;
for (var padding = 0; padding < (1 << paddingCount); padding++)
    table[(padding << codelen) | code] = (symbol << 5) + codeLen;

[1]:最大代码长度过长会使解码表变得非常大,MAX_CODELEN > 25 有溢出缓冲区的风险。有一些方法可以解决这个问题,但超长符号无论如何都不是很有用。

[2]:这不是 DELFATE 所做的。

【讨论】:

  • “这实际上与霍夫曼减压的“休息”相互作用。”不知道你的意思是什么。并且“这不是 DELFATE 所做的”不确定您的意思是这是一件好事还是只是不同。
  • @LancePollard 我只是说这不是一个可分离的问题。我们不能将比特流分成代码然后解码它们。这些步骤本质上是相互关联的,并且以某种方式改变了问题:实际上根本没有代码拆分。从缓冲区读取的块不是代码,它们通常包含额外的尾随位。 DEFLATE 反过来打包字节,我也可以告诉你
【解决方案2】:

你已经有了一个很好的阅读位的答案。

为了完整起见,如果您还想研究压缩,这里有一个(未经测试的)输出函数,可能有助于一些想法:

let remainder = 0; 
let remainderBits = 0;  // number of bits held in remainder

function putHuffman( 
    code,       // non-negative huffman code
    nBits) {    // bit length of code

    if( remainderBits) {
        code = code * (2 ** remainderBits) + remainder;
        nBits += remainderBits;
    }
    while( nBits >= 8) {
        putOctet( code % 256);
        code /= 256;
        nBits -=8;
    }
    remainder = code;
    remainderBits = nBits;
}

function putOctet( byte) {
    // add byte to the output stream
}

它可以转换为使用位移运算符,但目前代码中最多允许大约 46 位 - 如果添加 7 个剩余位,则位数达到 53,这是双浮点数中尾数值的最大位精度。

当然,由于 JavaScript 缺少整数数据类型,它不太适合密集的位运算 - 使用浮点乘法似乎并不比左移慢得多,如果它确实更慢的话。

【讨论】:

    猜你喜欢
    • 2023-03-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多