优化布尔逻辑树计算

Optimizing boolean logic tree evaluation

提问人:SyntaxT3rr0r 提问时间:4/11/2011 更新时间:4/12/2011 访问量:3267

问:

我有很多真/假结果保存为数组中的位。我确实有大量的这些(数以百万计的多头)。long[]

例如,假设我只有五个结果,我会有:

+----- condition 5 is true
|
|+---- condition 4 is false
||
||+--- condition 3 is true
|||
|||+-- condition 2 is true
||||
||||+- condition 1 is false
10110

我也有几棵树代表以下语句:

condition1 AND (condition2 OR (condition3 AND condition 4))

这些树很简单,但很长。它们基本上看起来像这样(下面过于简单化了,只是为了展示我所拥有的):

class Node {    
    int operator();
    List<Node> nodes;
    int conditionNumber();    
}

基本上,节点是一个叶子,然后有一个条件号(匹配 long[] 数组中的一个位),或者节点不是叶子,因此引用几个子节点。

它们很简单,但允许表达复杂的布尔表达式。效果很好。

到目前为止一切顺利,一切正常。但是我确实有一个问题:我需要评估很多表达式,确定它们是真是假。基本上,我需要对一个问题进行一些暴力计算,对于这个问题,没有比暴力破解更好的解决方案。

因此,我需要走一走树并回答,或者根据树的内容和内容来回答。truefalselong[]

我需要优化的方法如下所示:

boolean solve( Node node, long[] trueorfalse ) {
   ...
}

其中,在第一次调用时,是根节点,然后显然是子节点(递归的,该方法调用自身)。nodesolve

知道我只有几棵树(可能多达一百棵左右),但有数百万棵树需要检查,我可以采取哪些步骤来优化它?long[]

明显的递归解决方案传递参数((子)树和 long[],我可以通过不将其作为参数传递来摆脱它),并且所有递归调用等都非常慢。我需要检查使用了哪个运算符(AND 或 OR 或 NOT 等),并且涉及相当多的 if/else 或 switch 语句。long[]

我不是在寻找另一种算法(没有),所以我不是在寻找从 O(x) 到 O(y),其中 y 会小于 x。

我正在寻找的是“x倍”加速:如果我能以5倍的速度编写代码,那么我将有5倍的加速,仅此而已,我会对此感到非常满意。

到目前为止,我看到的唯一增强功能——我认为与我现在拥有的相比,这将是一个巨大的“x倍”加速——将为每棵树生成字节码,并将每棵树的逻辑硬编码到一个类中。它应该工作得很好,因为我只有一百棵左右的树(但这些树不是固定的:我无法提前知道树会是什么样子,否则简单地手动对每棵树进行硬编码是微不足道的)。

除了为每棵树生成字节码之外,还有什么想法吗?

现在,如果我想尝试字节码生成路由,我应该怎么做?

Java 优化 逻辑 字节码操作

评论

0赞 SyntaxT3rr0r 4/11/2011
对于好奇的人来说,我的 Node 实现与此没有什么不同:stackoverflow.com/questions/1830925
0赞 Jeff Foster 4/11/2011
一件显而易见的事情是尽快短路(例如,FALSE 和 anything is FALSE and TRUE or anything is TRUE)。我假设你已经这样做了?
1赞 SyntaxT3rr0r 4/11/2011
@Jeff 福斯特:是的,是的,我正在做:)但是可能有一些相对较大的树由很多 OR 语句组成,而只有几个 AND,所以即使使用短路,我仍然想改进这一点。:)
0赞 sehe 4/11/2011
如果您认为将树预编译为字节码是可行的,只需全长并生成机器指令(在 C 中,使用 JNI)。恕我直言,仅布尔值域在组装中足够简单,可以使这种风险/收益权衡发挥作用
0赞 phkahler 4/12/2011
您是否使用多组值评估同一棵树?

答:

4赞 sehe 4/11/2011 #1

为了最大限度地利用快捷方式评估的机会,您需要自己进行分支预测。

你可能想分析它,统计

  • 其中 AND 分支计算为 false
  • 哪个 OR 分支结果为 true

然后,您可以根据在性能分析步骤中找到的权重对树进行重新排序。如果你想要/需要特别漂亮,你可以设计一种机制,在运行时检测某个数据集的权重,这样你就可以动态地对分支进行重新排序。

请注意,在后一种情况下,建议不要对实际树进行重新排序(在执行时就存储效率结果的正确性而言),而是设计一个树节点访问者(遍历算法),该访问器能够根据“实时”权重对分支进行本地排序。

我希望这一切都是有道理的,因为我意识到散文版本是密集的。然而,就像费马说的,代码示例太大了,无法适应这个边距:)

评论

0赞 SyntaxT3rr0r 4/11/2011
很酷的答案,但是......long[] 数组树都是动态生成的。在这种情况下,您建议的分析是否仍然有效?例如,我可以将其与字节码生成结合起来吗?
0赞 sehe 4/11/2011
事物是动态的,因此很难进行任何预测。通过优化恒定的性能成本(即按原样编译为字节码/汇编),您可能会获得更好的成本/收益
1赞 Mike Dunlavey 4/11/2011 #2

我认为你的字节编码想法是正确的方向。 无论使用哪种语言,我都会做的是编写一个预编译器。 它会遍历每棵树,并使用 print 语句将其转换为源代码,例如。

((word&1) && ((word&2) || ((word&4) && (word&8))))

每当树发生变化时,都可以即时编译,并加载生成的字节码/ dll,所有这些都需要不到一秒钟的时间。

问题是,目前你正在解释树的内容。 将它们转换为编译代码应该使它们的运行速度提高 10-100 倍。

已添加,以回应您关于没有 JDK 的评论。然后,如果你不能生成 Java 字节码,我会尝试编写我自己的字节码解释器,而不是尽可能快地运行。它可能看起来像这样:

while(iop < nop){
  switch(code[iop++]){
    case BIT1: // check the 1 bit and stack a boolean
      stack[nstack++] = ((word & 1) != 0);
      break;
    case BIT2: // check the 2 bit and stack a boolean
      stack[nstack++] = ((word & 2) != 0);
      break;
    case BIT4: // check the 4 bit and stack a boolean
      stack[nstack++] = ((word & 4) != 0);
      break;
    // etc. etc.
    case AND: // pop 2 booleans and push their AND
      nstack--;
      stack[nstack-1] = (stack[nstack-1] && stack[nstack]);
      break;
    case OR: // pop 2 booleans and push their OR
      nstack--;
      stack[nstack-1] = (stack[nstack-1] || stack[nstack]);
      break;
  }
}

这个想法是让编译器将开关变成跳转表,因此它以最少的循环数执行每个操作。 要生成操作码,您只需对树进行后缀遍历即可。

最重要的是,你可以通过对德摩根定律的一些操作来简化它,这样你就可以一次检查多个位。

评论

0赞 SyntaxT3rr0r 4/12/2011
好的,谢谢:)哦,好吧,那我将尝试学习 Java 的 ASM 字节码库,看看是什么:)
0赞 Mike Dunlavey 4/12/2011
@SyntaxT3rr0r:你可以,但我不会。我会生成源代码,让编译器生成低级的东西。这已经足够快了。
0赞 SyntaxT3rr0r 4/12/2011
可悲的是,我可能无法完全控制安装在运行:(的机器上的JDK(如果有的话)
0赞 Mike Dunlavey 4/12/2011
@SyntaxT3rr0r:好吧,这让事情变得更难了。Java 字节码将是您的下一个最佳选择。如果你做不到这一点,那么你可以自己做口译员,正如我上面概述的那样。
0赞 SyntaxT3rr0r 4/12/2011
我喜欢你的编辑...但是我检查了一下,Java 的 ASM 库似乎只有 50 KB。我不知道什么是最好的自动取款机。我将评估这两个选项:)但我认为,出于便利,我将首先从您在编辑:)中的建议开始
3赞 phkahler 4/12/2011 #3

在 C 语言中,有一种简单而快速的方法来计算布尔运算,假设你想计算 z=(x op y),你可以这样做:

 z = result[op+x+(y<<1)];

因此,op 将是 4 的倍数来选择您的操作 AND、OR、XOR 等,您为所有可能的答案创建一个查找表。如果此表足够小,则可以将其编码为单个值,并使用右移和掩码选择输出位:

z = (MAGIC_NUMBER >> (op+x+(y<<1))) & 1;

这将是评估大量此类内容的最快方法。当然,您必须将具有多个输入的操作拆分为每个节点只有 2 个输入的树。然而,没有简单的方法可以将其短路。您可以将树转换为列表,其中每个项目都包含操作编号以及指向 2 个输入和输出的指针。一旦以列表形式出现,您可以使用单个循环非常快速地将该行吹过一百万次。

对于小树来说,这是一场胜利。对于短路的大树来说,这可能不是一个胜利,因为需要评估的平均树枝数量从 2 个到 1.5 个,这对大树来说是一个巨大的胜利。YMMV。

编辑:仔细想想,您可以使用跳过列表之类的东西来实现短路。每个操作(节点)将包括一个比较值和一个跳过计数。如果结果与比较值匹配,则可以绕过下一个跳过计数值。因此,该列表将根据树的深度优先遍历创建,并且第一个子项将包含与另一个子项大小相等的跳过计数。这会给每个节点评估带来一些复杂性,但允许短路。仔细的实现可以在没有任何条件检查的情况下做到这一点(想想跳过计数的 1 倍或 0 倍)。

评论

0赞 SyntaxT3rr0r 4/12/2011
这种东西让我真的很怀念我的集会和 C 日子。可悲的是,我现在“被 Java 困住了”:(但是+1绝对是不错的答案!
0赞 phkahler 4/12/2011
@SyntaxT3rr0r:谢谢。我曾经用它来进行数字逻辑仿真。请参阅列表的短路编辑;-)