优化布尔逻辑树评估

时间:2011-04-11 12:13:34

标签: java optimization boolean-logic bytecode-manipulation

我有很多真/假结果保存为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();    
}

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

它们很简单但它们允许表达复杂的布尔表达式。它很棒。

到目前为止,一切都很好。但是我确实有一个问题:我需要评估表达式的 LOT ,确定它们是真还是假。基本上我需要对一个问题进行一些强力计算,而这个问题除了强制执行之外还没有更好的解决方案。

所以我需要走树并回答truefalse,具体取决于树的内容和long[]的内容。

我需要优化的方法如下:

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

在第一次调用时,node是根节点,然后显然是子节点(递归,solve方法调用自身)。

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

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

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

我正在寻找的是“次x”加速:如果我能编写速度提高5倍的代码,那么我将获得5倍的加速,那就是它,我会非常开心用它。

我现在看到的唯一增强 - 而且我认为与我现在相比,它将是一个巨大的“倍x”加速 - 将为每个树生成字节码拥有硬编码到类中的每棵树的逻辑。它应该工作得很好,因为我只会有一百棵左右的树(但是树木没有修复:我事先无法知道树木会是什么样子,否则简单地手动硬编码每棵树都是微不足道的)。

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

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

3 个答案:

答案 0 :(得分:3)

为了最大化快捷评估的机会,您需要进行自己的分支预测。

您可能想要对其进行分析,统计

  • 哪个AND分支评估为假
  • OR分支结果为真

然后,您可以相对于在分析步骤中找到的权重重新排序树。如果您希望/需要特别漂亮,您可以设计一种机制,在运行时检测某个数据集的权重,以便您可以动态地重新排序分支。

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

我希望所有这一切都有道理,因为我意识到散文版很密集。然而,就像Fermat所说的那样,代码示例太大而无法适应这个范围:)

答案 1 :(得分: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倍)。

答案 2 :(得分:1)

我认为你的字节编码理念是正确的方向。 不管语言如何,我会做的是写一个预编译器。 它将遍历每个树,并使用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;
  }
}

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

最重要的是,您可以通过对De Morgan定律的一些操纵来简化它,因此您可以一次检查多个位。