Performance 在Prolog中将谓词应用于列表的子集:请求关于实现选择的建议

Performance 在Prolog中将谓词应用于列表的子集:请求关于实现选择的建议,performance,list,prolog,filtering,tail-recursion,Performance,List,Prolog,Filtering,Tail Recursion,编辑:在我提到列出的第二个是我第一次尝试实施的项目后,我使用术语“第一次实施”和“第二次实施”时,出现了一些可以理解的混乱,因此我重新编写了相关段落。很抱歉给你带来了困惑 一些动机背景:我正在SWI Prolog中构建约束求解器,为了极大地优化空间和时间,我在主要约束数据结构中构建了反向索引。每当我的系统中的一个“变量”(不是Prolog变量)被赋值时,我想确保这个赋值不会使任何其他约束无法满足。我有一个从变量到约束的索引,可以快速选择要检查的约束。从某种意义上讲,这归结为将try_check/

编辑:在我提到列出的第二个是我第一次尝试实施的项目后,我使用术语“第一次实施”和“第二次实施”时,出现了一些可以理解的混乱,因此我重新编写了相关段落。很抱歉给你带来了困惑

一些动机背景:我正在SWI Prolog中构建约束求解器,为了极大地优化空间和时间,我在主要约束数据结构中构建了反向索引。每当我的系统中的一个“变量”(不是Prolog变量)被赋值时,我想确保这个赋值不会使任何其他约束无法满足。我有一个从变量到约束的索引,可以快速选择要检查的约束。从某种意义上讲,这归结为将try_check/2谓词应用于给定的左侧(LHS)和右侧列表(RHS_L)的所有元素,其索引显示在列表(IdxL)中。以下是我当前的实现:

%% FORALL Implementation
try_check_filtered(LHS, IdxL, RHS_L) :-
    forall((member(Idx, IdxL), nth0(Idx, RHS_L, RHS)), 
           try_check(LHS, RHS)).
我还有一个早期的实现,它做了同样的事情,但是在第二个位置使用了一个额外的参数来跟踪当前的列表索引(索引列表按升序排序):

我有两个问题:

  • 有没有更好的方法来实现这一点,我想不出来
  • 我惊讶地发现forall实现比tail递归实现性能更好。在我看来,尾部递归具有线性时间调度(在列表中遍历一次以“调用”所有必要的try\u检查),而forall实现具有二次调度(对成员的调用的线性数量,每个调用导致对nth0的另一个线性调用)。(如果您对我所看到的性能改进感到好奇,在执行过程中大约需要44秒,使用forall实现over the tail recursive on可以节省大约4秒)
  • 我的一个想法是,尾部递归优化没有应用到我的尾部递归实现中,将列表的多个副本强制到堆栈帧上,从而使其速度变慢。 为了(希望)为我的尾部递归实现启用尾部递归优化,我尝试通过在规则末尾添加一个cut(!)来确定try\u check/2谓词。够了吗?try_check/2规则有临时的副作用是否重要:它断言一些事实,并在完成之前收回这些事实,从而使事实集合保持不变。我上面报告的性能与try\u check/2谓词中的cut有关

    我希望我提供了足够的信息进行建设性的讨论。提前感谢您的回复

    编辑:这里是try\u check的(高级)实现。整个代码有2600行,其中许多(可能有一半)是此检查间接使用的,因此无法在此处发布

    %% try_check_eni(+E1:effect, +E2:effect)
    try_check_eni(E1, E2) :-
        push_trying,
        check_no_try,
        ( is_non_interfering(E1, E2)
        -> (clear_try, pop_trying)
        ; (clear_try, pop_trying, fail))
        , !.
    

    push_-trying/0
    pop_-trying/0
    断言和收回一个
    trying/0
    谓词,该谓词稍微修改了其他一些谓词的操作方式,这样我就不必重复检查try_-check谓词时使用的代码<代码>不干扰/2不确定。在
    try
    模式下,
    是非干扰的
    将实例化变量标记为
    try/1
    ,以便在检查约束后
    清除\u try/0
    可以收回实例化

    nth0/3
    具有线性成本。因此,与
    member/2
    forall/2
    的组合具有二次成本。事实上,第二个代码变量没有利用索引列表按升序排列这一事实,而第一个代码变量则利用了这一事实

    在开发的这个阶段,尽量不要过度优化:首先要正确执行,然后(并且只有这样)快速执行

    关注清晰可读的代码,选择适当的数据结构,做出正确的库选择。。。如果环境需要,您可以用(比如)一些复合结构加上
    arg/3
    替换列表中的随机读取访问


    此外,您的代码可能会从第一个参数索引中获益。使用cut和/或assert/RECTract时要小心,这两种方法都很容易降低正确性和性能。

    感谢您的回复,但我意识到我的帖子不清楚。在使用术语1和2实施时,我指的是问题中列出它们的顺序,而不是我实施它们的时间顺序。所以我(仍然)很困惑,为什么二次型(forall)比线性型(tail recursive)好。我编辑了这个问题,添加了一些您要求的信息。不幸的是,代码太多,无法提供完整的实现。它是一个具有相对复杂规则的解算器,用于确定域的可满足性(如果感兴趣,请查找确定性并行java或DPJ)。我希望我提供的信息至少有点帮助。
    %% try_check_eni(+E1:effect, +E2:effect)
    try_check_eni(E1, E2) :-
        push_trying,
        check_no_try,
        ( is_non_interfering(E1, E2)
        -> (clear_try, pop_trying)
        ; (clear_try, pop_trying, fail))
        , !.