【问题标题】:JavaScript base64string -> binary values -> [Integer] ... performance improvementJavaScript base64string -> 二进制值 -> [Integer] ... 性能提升
【发布时间】:2021-07-11 06:17:37
【问题描述】:

我正在使用 Parse Server 并尝试加快使用布隆过滤器的查询。

每个文档都有一个字段bf,其数值范围为0...bloomSize,例如文档ID“xyz”被散列为bf = 6462

查询然后加载二进制布隆过滤器值,这些值被编码并保存在 base64 字符串中。为了在 Parse Server / MongoDB 中使用索引查询,我需要生成一个整数数组,然后可以将其与上述字段进行比较。因此需要对 base64 字符串进行解码,并且对于二进制数据中的每个 0,我必须附加该 0 值位置的整数。目前我正在使用以下 sn-p:

//loading [UInt8] from saved base64 string
const buf = new Buffer.from(request.user.get("daBlm"), 'base64');

var blm = Array()

for (var i = 0; i < buf.length; i++) {
    //iterating through 8-bit chunks and convert each UInt8 into string
    //as "toString" does not respect 8-bit length we chain ".padStart(8, '0')"
    const byte = buf[i].toString(2).padStart(8, '0');
    //loop through characters and for each 0, push the position into array
    for (var l = 0; l < byte.length; l++) {
        //push int to array for each bloom 0 value - not used bit
        if (byte[l] == "0") {
            blm.push(i*8 + l);
        } 
    }
}
//at the end is the blm array user in containedIn query constraint that use indexes    
dateQuery.containedIn("bf", blm);

虽然它按预期工作并且我的查询使用索引,但数组的生成是查询中最慢的部分。

我发现在进入循环之前设置数组长度会稍微加快速度:

//从保存的base64字符串加载[UInt8] const buf = new Buffer.from(request.user.get("daBlm"), 'base64');

var blm = Array()
blm.lenght = buf.lenght * 8  <-- this seems to help
for (var i = 0; i < buf.length; i++) {
    //iterating through 8-bit chunks and convert each UInt8 into string
    //as "toString" does not respect 8-bit length we chain ".padStart(8, '0')"
    const byte = buf[i].toString(2).padStart(8, '0');
    //loop through characters and for each 0, push the position into array
    for (var l = 0; l < byte.length; l++) {
        //push int to array for each bloom 0 value - not used bit
        if (byte[l] == "0") {
            blm.push(i*8 + l);
        } 
    }
}

我没有注意到数组的constvar 之间有任何区别。

我也尝试使用 switch 直接从 base64 生成,但这似乎比上面的循环慢了大约 10%-30%:

function availArr(base64Str) {
    const lenght = base64Str.length;
    if (lenght > 0) {
        var arr = Array();
        
        for (var i = 0; i < lenght; i++) {
            //iterating through characters of base64String
            switch (base64Str.charAt(i)) {
                case "A": //000000
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+3, i*8+4, i*8+5]);break;
                case "B": //000001
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+3, i*8+4]);break;
                case "C": //000010
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+3, i*8+5]);break;
                case "D": //000011
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+3]);break;
                case "E": //000100
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+4, i*8+5]);break;
                case "F": //000101
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+4]);break;
                case "G": //000110
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+5]);break;
                case "H": //000111
                    arr.push(...[i*8, i*8+1, i*8+2]);break;
                case "I": //001000
                    arr.push(...[i*8, i*8+1, i*8+3, i*8+4, i*8+5]);break;
                case "J": //001001
                    arr.push(...[i*8, i*8+1, i*8+3, i*8+4]);break;
                case "K": //001010
                    arr.push(...[i*8, i*8+1, i*8+3, i*8+5]);break;
                case "L": //001011
                    arr.push(...[i*8, i*8+1, i*8+3]);break;
                case "M": //001100
                    arr.push(...[i*8, i*8+1, i*8+4, i*8+5]);break;
                case "N": //001101
                    arr.push(...[i*8, i*8+1, i*8+4]);break;
                case "O": //001110
                    arr.push(...[i*8, i*8+1, i*8+5]);break;
                case "P": //001111
                    arr.push(...[i*8, i*8+1]);break;
                case "Q": //010000
                    arr.push(...[i*8, i*8+2, i*8+3, i*8+4, i*8+5]);break;
                case "R": //010001
                    arr.push(...[i*8, i*8+2, i*8+3, i*8+4]);break;
                case "S": //010010
                    arr.push(...[i*8, i*8+2, i*8+3, i*8+5]);break;
                case "T": //010011
                    arr.push(...[i*8, i*8+2, i*8+3]);break;
                case "U": //010100
                    arr.push(...[i*8, i*8+2, i*8+4, i*8+5]);break;
                case "V": //010101
                    arr.push(...[i*8, i*8+2, i*8+4]);break;
                case "W": //010110
                    arr.push(...[i*8, i*8+2, i*8+5]);break;
                case "X": //010111
                    arr.push(...[i*8, i*8+2]);break;
                case "Y": //011000
                    arr.push(...[i*8, i*8+3, i*8+4, i*8+5]);break;
                case "Z": //011001
                    arr.push(...[i*8, i*8+3, i*8+4]);break;
                case "a": //011010
                    arr.push(...[i*8, i*8+3, i*8+5]);break;
                case "b": //011011
                    arr.push(...[i*8, i*8+3]);break;
                case "c": //011100
                    arr.push(...[i*8, i*8+4, i*8+5]);break;
                case "d": //011101
                    arr.push(...[i*8, i*8+4]);break;
                case "e": //011110
                    arr.push(...[i*8, i*8+5]);break;
                case "f": //011111
                    arr.push(...[i*8]);break;
                case "g": //100000
                    arr.push(...[i*8+1, i*8+2, i*8+3, i*8+4, i*8+5]);break;
                case "h": //100001
                    arr.push(...[i*8+1, i*8+2, i*8+3, i*8+4]);break;
                case "i": //100010
                    arr.push(...[i*8+1, i*8+2, i*8+3, i*8+5]);break;
                case "j": //100011
                    arr.push(...[i*8+1, i*8+2, i*8+3]);break;
                case "k": //100100
                    arr.push(...[i*8+1, i*8+2, i*8+4, i*8+5]);break;
                case "l": //100101
                    arr.push(...[i*8+1, i*8+2, i*8+4]);break;
                case "m": //100110
                    arr.push(...[i*8+1, i*8+2, i*8+5]);break;
                case "n": //100111
                    arr.push(...[i*8, i*8+1, i*8+2, i*8+3, i*8+4, i*8+5]);break;
                case "o": //101000
                    arr.push(...[i*8+1, i*8+3, i*8+4, i*8+5]);break;
                case "p": //101001
                    arr.push(...[i*8+1, i*8+3, i*8+4]);break;
                case "q": //101010
                    arr.push(...[i*8+1, i*8+3, i*8+5]);break;
                case "r": //101011
                    arr.push(...[i*8+1, i*8+3]);break;
                case "s": //101100
                    arr.push(...[i*8+1, i*8+4, i*8+5]);break;
                case "t": //101101
                    arr.push(...[i*8+1, i*8+4]);break;
                case "u": //101110
                    arr.push(...[i*8+1, i*8+5]);break;
                case "v": //101111
                    arr.push(...[i*8+1]);break;
                case "w": //110000
                    arr.push(...[i*8+2, i*8+3, i*8+4, i*8+5]);break;
                case "x": //110001
                    arr.push(...[i*8+2, i*8+3, i*8+4]);break;
                case "y": //110010
                    arr.push(...[i*8+2, i*8+3, i*8+5]);break;
                case "z": //110011
                    arr.push(...[i*8+2, i*8+3]);break;
                case "0": //110100
                    arr.push(...[i*8+2, i*8+4, i*8+5]);break;
                case "1": //110101
                    arr.push(...[i*8+2, i*8+4]);break;
                case "2": //110110
                    arr.push(...[i*8+2, i*8+5]);break;
                case "3": //110111
                    arr.push(...[i*8+2]);break;
                case "4": //111000
                    arr.push(...[i*8+3, i*8+4, i*8+5]);break;
                case "5": //111001
                    arr.push(...[i*8+3, i*8+4]);break;
                case "6": //111010
                    arr.push(...[i*8+3, i*8+5]);break;
                case "7": //111011
                    arr.push(...[i*8+3]);break;
                case "8": //111100
                    arr.push(...[i*8+4, i*8+5]);break;
                case "9": //111101
                    arr.push(...[i*8+4]);break;
                case "+": //111110
                    arr.push(...[i*8+5]);break;
            }
        }
        return arr;
    }
    return [];
}

有人知道如何加快速度吗?

EDIT2+3

我将函数移动到单独的代码中,它的行为保持不变,但允许我在其他函数中重用它。后来根据@Jonas 的建议,我试图避免字符串转换:

Buffer.prototype.toIntegerArray = function() {
    const buffLenght = this.length;
    if (buffLenght > 0) {
        const arr = Array();
        arr.lenght = buffLenght * 8
        for (var i = 0; i < buffLenght; i++) {
            //iterating through 8-bit chunks
            for (var l = 0; l < 8; l++) {
                //push int to array for each bloom 0 value - not used bit
                //corrected syntax
                if (((this[i] >> l) & 1) === 0) {
                    arr.push(i*8 + l);
                }
            }
        }
        return arr;
    }
    return [];
}

根据@Jonas 的评论,正确的语法确实找到了零。通过控制台时间戳测量查询的数组构建部分,平均 10 个查询响应,65536 位绽放大小全为 0(最终随机半盛开,对所有人都一样):

  1. toString(2).padStart(8, '0') => ~ 303ms (152ms)
  2. switch => ~322ms (145ms)
  3. (((this[i] &gt;&gt; l) &amp; 1) === 0) => ~340ms
  4. (var mask = 0x80; mask &gt; 0; mask&gt;&gt;=1) => ~354ms
  5. if ((byte &amp; 0x80) === 0) arr.push(counter); => 314 毫秒 (158 毫秒)

正如我从测量结果中注意到的那样,循环遍历全 0s 数组和随机半开花有很大的不同。因此,我猜更大的部分是推动本身。

我了解测量结果部分具有误导性,但已给出环境(解析服务器云功能),遗憾的是我没有经验在那里进行更复杂的调查。越难成为 DBaaS 上的共享服务器。

【问题讨论】:

  • if((buf[i] &gt;&gt; l) &amp; 1 === 0) 还应该检查第 l 位是否为 0
  • 作为另一个想法,base64 中的一个字符似乎总是被解码为 6 位二进制。因此可以生成一个从字符到零索引数组的查找表。遍历base64,查找并将这些结果推送到数组。
  • @Jonas 如果是您的第一条评论,我仍然不会摆脱这两个循环,对吧?如果我理解正确,它会将转换保存为字符串和 padStart?对于第二个想法,我读到 switch 的性能比查找表好,我尝试过这种方式,不幸的是性能并没有好多少。我会将其添加到我的问题中。
  • 如果代码有效,您可以检查their help center 以查看您的问题是否与Code Review 的主题有关。
  • 啊,我的第一条评论由于运算符优先级而不起作用,添加另一对大括号(((this[i] &gt;&gt; l) &amp; 1) === 0)

标签: javascript binary base64


【解决方案1】:

当您避免使用.toString(2) 转换为字符串时,它应该会有所改善。也可以通过使用单独的计数器变量来避免重复的i*8+l

for (var i = 0, counter = 0; i < buf.length; i++) {
    var byte = buf[i];
    for (var mask = 0x80; mask > 0; mask>>=1) {
        if ((byte & mask) === 0) {
            blm.push(counter);
        }
        counter++;
    }
}

由于内部循环只有 8 次迭代,您还可以将代码复制 8 次,从而减少移位操作:

for (var i = 0, counter = 0; i < buf.length; i++, counter+=8) {
    var byte = buf[i];
    if ((byte & 0x80) === 0) blm.push(counter);
    if ((byte & 0x40) === 0) blm.push(counter+1);
    if ((byte & 0x20) === 0) blm.push(counter+2);
    if ((byte & 0x10) === 0) blm.push(counter+3);
    if ((byte & 0x08) === 0) blm.push(counter+4);
    if ((byte & 0x04) === 0) blm.push(counter+5);
    if ((byte & 0x02) === 0) blm.push(counter+6);
    if ((byte & 0x01) === 0) blm.push(counter+7);
}

跳过 base64 解码的想法也是一个很好的尝试,尽管这意味着在纯 JavaScript 中迭代的字符串更长(而不是在更低、更快的级别上发生这种情况)。尽管如此,在您的尝试中仍有一些需要改进的地方:构建一个数组然后传播它是浪费时间。您可以将这些参数直接传递给push。例如:

case "A": //000000
    arr.push(i*8, i*8+1, i*8+2, i*8+3, i*8+4, i*8+5);break;

此外,应该避免这种重复的i*8(与上述相同的解决方案)。

【讨论】:

  • 感谢您的意见。也许我的测量受到客户端/服务器上的实际负载的某种影响,但您的解决方案的排名仍然略低于@Jonas。我在某处读到,将大循环分成子循环实际上会有所帮助(“Duff 的东西......?)也许这里有一个额外的计数器持有一些参考?我是编程初学者,对不起。
  • 这是微优化,然后取决于您在哪个 JavaScript 引擎上运行...它的优化器如何工作...我添加了另一个变体。如果需要进一步提高速度,请考虑转向编译语言。 C 或 C++ 或一起删除 base64 编码(不确定它有什么用途)。
  • 正如我从测量结果中注意到的,循环遍历全 0s 数组和随机半满开花之间存在很大差异。因此,我猜更大的部分是推动本身。不幸的是,该环境现在由 Parse Server 定义,也许稍后我可以转移到 ElasticSearch,在那里我可以提供布隆过滤器的“已使用”部分,而不是更大数组的“可用”部分。
  • 确实,你拥有的零越多,需要生成的数组就越长,所以没有办法。
猜你喜欢
  • 1970-01-01
  • 2011-08-20
  • 2020-12-20
  • 2012-09-04
  • 2021-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-30
相关资源
最近更新 更多