首页 > 解决方案 > 了解 Phreak 算法的性能

问题描述

我试图了解是什么让 Drools 在我们的用例中表现不佳。我在这里阅读了 Phreak 文档:https ://access.redhat.com/documentation/en-us/red_hat_decision_manager/7.4/html/decision_engine_in_red_hat_decision_manager/phreak-algorithm-con_decision-engine

它似乎没有提到关于如何完成节点到节点跳转的任何内容。这是一个示例来解释我的意思:

假设我有一个包含三个字段的 Person 对象:name、lastName、SSN

我这样定义了大量规则:当 lastName = 'Smith' 和 SSN = 'XXXXXXXXXX' 然后 name = "Jane"。假设我有很多人以“Smith”作为姓氏,比如 10,000,那么获得一个姓氏和 SSN 的单一名字的复杂性是多少?是否需要 10,000 次比较,或者“Smith”节点是否与所有底层 SSN 保持某种形式的哈希映射?

如果我使用范围(例如日期范围)而不是具有相等运算符的 SSN,会如何定义如下规则:“当 lastName = 'Smith' and startedShool >= 2005 and finishedSchool <= 2010”。节点是否保留一些花哨的数据结构来加快使用范围运算符的查询?

编辑:根据请求,我添加了一个示例来解释规则是如何设置的

我们有一个名为 Evidence 的类作为输入和输出。我们在不同的激活组中构建每个证据实例并将其添加到集合中。我们通常定义一些包罗万象的、低显着性的广泛规则,并添加具有更具体条件和更高显着性的规则。

这是两个显着性增加的规则的示例。我们定义了其中的约 10K。

可以想象一种树结构,在每个级别上,显着性都在增加,条件变得更加严格。Evidence 类作为一种键值对起作用,因此它们中的许多将具有相同的共同节点(例如,名称=位置)。

要执行规则,我们将添加两个证据(例如 [name= location, stringValue='BBB'] 和 [name = code, stringValue = theCode])并触发规则。

rule "RULE 1::product"
    salience 0
    activation-group "product"
when
    $outputs : EvidenceSet.Builder(  )  

    Evidence( name == "location" && stringValue == "BBB" )   
then
    $outputs.addEvidences(Evidence.newBuilder().setName("Product").setStringValue("999"));


end

rule "RULE 2::product"
    salience 1
    activation-group "product"
when
    $outputs : EvidenceSet.Builder(  )  

    Evidence( name == "location" && stringValue == "BBB" )   

    Evidence( name == "code" && stringValue == "thecode" )   
then
    $outputs.addEvidences(Evidence.newBuilder().setName("Product").setStringValue("777"));


end

标签: drools

解决方案


这是规则模板

rule "RULE %s::product"
when
    $outputs : EvidenceSet.Builder(  )  
    Evidence( name == "location" && stringValue == "BBB" ) 
    Evidence( name == "code" && stringValue matches ".*code.*" )
then
    $outputs.addEvidences(Evidence.newBuilder().setName("Product").setStringValue("777"));
end

这是从模板中构建 10000 条规则 drl 的代码

public static void main(String[] args) throws IOException {
    String template = Resources.toString(Resources.getResource("template.drl"), Charset.defaultCharset());
    
    try (PrintStream file = new PrintStream(new File("test.drl"))) {
        for (int i = 0; i < 10_000; i++)
            file.println(String.format(template, i));
    }
}

这是基于您的 drl 语法的域类,不包括 equals、hashCode 和 toString,对我来说看起来很奇怪......

public class Evidence {
    public String name;
    public String stringValue;
    
    public Evidence() {
    }
    
    public Evidence(String name, String stringValue) {
        this.name = name;
        this.stringValue = stringValue;
    }
    
    public static Builder newBuilder() {
        return new Builder();
    }
}

public class EvidenceSet {
    public static Set<Evidence> evidenceSet = new HashSet<>();
    
    public static class Builder {
        public Evidence evidence = new Evidence();
        
        public Builder setName(String name) {
            evidence.name = name;
            return this;
        }
        
        public Builder setStringValue(String stringValue) {
            evidence.stringValue = stringValue;
            return this;
        }
        
        public void addEvidences(Builder builder) {
            evidenceSet.add(builder.evidence);
        }
    }
}

这是执行 10k 规则文件的测试

@DroolsSession("classpath:/test.drl")
public class PlaygroundTest {
    
    private PerfStat firstStat = new PerfStat("first");
    private PerfStat othersStat = new PerfStat("others");
    @Rule
    public DroolsAssert drools = new DroolsAssert();
    
    @Test
    public void testIt() {
        firstStat.start();
        drools.insertAndFire(new EvidenceSet.Builder(), new Evidence("location", "BBB"), new Evidence("code", "thecode"));
        firstStat.stop();
        
        for (int i = 0; i < 500; i++) {
            othersStat.start();
            drools.insertAndFire(new Evidence("code", "code" + i));
            othersStat.stop();
        }
        
        System.out.printf("first, ms: %s%n", firstStat.getStat().getAvgTimeMs());
        System.out.printf("others, ms: %s%n", othersStat.getStat().getAvgTimeMs());
        System.out.println(EvidenceSet.evidenceSet);
    }
}

规则和测试应该符合您的要求,以匹配规则中的所有决策节点。议程组和显着性与性能无关,为简单起见此处略过。

没有“for”部分的测试,给出以下分析结果 profile1.png

“赢家”是org.drools.core.rule.JavaDialectRuntimeData.PackageClassLoader.internalDefineClass为drools创建和注册自动生成的java类然后阻塞,这是重量级操作。

带有“for”部分的测试给出了完全不同的画面 profile2.png

通知internalDefineClass保持不变,测试代码执行变得显着。因为加载了 10k 规则类,并且在我的 6 核(12 个逻辑)CPU 机器上花费了 12.5 秒。之后,所有其他插入均在 avg 中执行。700 毫秒。让我们分析一下那个时候。

然后规则块在平均 0.01 毫秒内执行。

RULE 9::product - min: 0.00 avg: 0.01 max: 1.98 activations: 501

10000 RHS * 0.01 ms = 100 ms是 10k 条规则执行以下操作所花费的时间

$outputs.addEvidences(Evidence.newBuilder().setName("Product").setStringValue("777"));

虚拟对象的创建和操作变得值得注意,因为您将其倍增至 10k。根据第二次性能分析结果,大多数时候它会将测试运行器输出打印到控制台。总共100毫秒。考虑到您粗鲁的对象创建服务于“构建器”范式,RHS 执行速度非常快。

要回答您的问题,我会猜测并提出以下问题:

  1. 您不得重用已编译的规则会话。

  2. 它可以被低估然后阻塞执行时间,如果你在 RHS 的某个地方有虚拟的 String.format ,那么整个执行时间将会值得注意,因为 format 比较慢并且你正在处理 10k 执行。

  3. 可能存在笛卡尔规则匹配。仅仅因为您使用集合作为结果,您只能猜测该集合中插入了多少“完全相同的结果”,这意味着巨大的执行浪费。

  4. 如果您使用的是全状态会话,我看不到任何内存清理和对象收回。这可能会在 OOME 之前导致性能问题。这是运行的内存占用...

个人资料3


day2...笛卡尔匹配

我删除了日志记录并对笛卡尔匹配进行了测试

@Test
public void testIt() {
    firstStat.start();
    drools.insertAndFire(new EvidenceSet.Builder(), new Evidence("location", "BBB"), new Evidence("code", "thecode"));
    firstStat.stop();
    
    for (int i = 0; i < 40; i++) {
        othersStat.start();
        drools.insertAndFire(new Evidence("location", "BBB"), new Evidence("code", "code" + i));
        othersStat.stop();
    }
    
    drools.printPerformanceStatistic();
    System.out.printf("first, ms: %s%n", firstStat.getStat().getAvgTimeMs());
    System.out.printf("others, ms: %s%n", othersStat.getStat().getAvgTimeMs());
    System.out.println(EvidenceSet.evidenceSet);
}

请注意,我们正在插入new Evidence("location", "BBB")每次迭代。我只能以 40 次迭代运行测试,否则我最终会使用 OOME(对我来说是新事物)。这是 40 次迭代的 CPU 和内存消耗:

资料4

每条规则被触发 1681 次!RHS 平均 时间并不显着(优化并从执行中删除?),但是当块被执行时......

RULE 999::product - min: 0.00 avg: 0.00 max: 1.07 activations: 1681
RULE 99::product - min: 0.00 avg: 0.00 max: 1.65 activations: 1681
RULE 9::product - min: 0.00 avg: 0.00 max: 1.50 activations: 1681
first, ms: 10271.322
others, ms: 1614.294

删除笛卡尔匹配后,相同的测试执行得更快,并且没有内存和 GC 问题

RULE 999::product - min: 0.00 avg: 0.04 max: 1.27 activations: 41
RULE 99::product - min: 0.00 avg: 0.04 max: 1.28 activations: 41
RULE 9::product - min: 0.00 avg: 0.04 max: 1.52 activations: 41
first, ms: 10847.358
others, ms: 102.015

简介5

现在我可以将迭代次数增加到 1000 次,并且迭代的平均执行时间更短

RULE 999::product - min: 0.00 avg: 0.00 max: 1.06 activations: 1001
RULE 99::product - min: 0.00 avg: 0.00 max: 1.20 activations: 1001
RULE 9::product - min: 0.00 avg: 0.01 max: 1.79 activations: 1001
first, ms: 10986.619
others, ms: 71.748

插入事实而不撤回将有它的限制

简介5

摘要:您需要确保没有得到笛卡尔匹配。


省略任何笛卡尔匹配的快速解决方案是保持唯一Evidence的 s。以下不是执行此操作的“常用方法”,但不需要更改规则条件并增加更多复杂性。

Evidence向类添加方法

public boolean isDuplicate(Evidence evidence) {
    return this != evidence && hashCode() == evidence.hashCode() && equals(evidence);
}

添加具有最高显着性的规则

rule "unique known evidence"
salience 10000
when
    e: Evidence() 
    Evidence(this.isDuplicate(e))
then
    retract(e);
end

这已经过测试,并且似乎可以通过重现笛卡尔问题的先前 junit 测试来工作。有趣的是,对于这个测试(1000 次迭代,1000 条规则),isDuplicate方法被调用了 2008004 次,所有调用所花费的总时间为 172.543 毫秒。在我的机器上。规则的 RHS(事件撤回)至少比其他规则(证据收集)长 3 倍。

RULE 999::product - min: 0.00 avg: 0.00 max: 1.07 activations: 1001
RULE 99::product - min: 0.00 avg: 0.01 max: 1.81 activations: 1001
RULE 9::product - min: 0.00 avg: 0.00 max: 1.25 activations: 1001
unique known evidence - min: 0.01 avg: 0.03 max: 1.88 activations: 1000
first, ms: 8974.7
others, ms: 69.197

推荐阅读