PL/SQL 的数值优化或替代方案

Posted

技术标签:

【中文标题】PL/SQL 的数值优化或替代方案【英文标题】:Numerical Optimization of PL/SQL, or alternatives 【发布时间】:2016-10-22 15:46:31 【问题描述】:

我们需要做一些繁重的计算工作来连接 Oracle 数据库。到目前为止,我们已经在 PL/SQL 中执行了我们的数值计算,并且很大程度上忍受了性能的不足。

现在我们正在实施多级库存模型 (https://www.researchgate.net/publication/222409130_Evaluation_of_time-varying_availability_in_multi-echelon_spare_parts_systems_with_passivation) 由于问题的规模,我更关心性能。

我已经用三种语言实现了部分算法:Fortran(90-2008 与 gfortran 兼容)、Excel 中的 VBA 和 PL/SQL,并围绕它包装了一百万次调用的测试循环。即使使用binary_double 数据类型和使用PLSQL_CODE_TYPE=NATIVE 的本机编译(两者都导致改进),下面的测试代码仍然需要37 秒才能运行(Oracle XE 11.2)。相比之下,VBA 在同一硬件上需要 16 秒,而 Fortran 需要 1.6 秒。

虽然要求性能接近 Fortran 数字可能太过分了(尽管这显然是非常可取的)但我很惊讶即使是不起眼的 VBA 也能胜过 PL/SQL。

所以我的问题有两个部分:

    还有什么我可以在 Oracle 方面尝试以进一步提高性能的方法吗? 如果我们考虑放弃 PL/SQL,我们应该考虑哪些替代方案来连接 Oracle 数据库?繁重的工作将相对很少以类似批处理模式的方式调用,尽管性能改进接近 Fortran 的速度将使我们能够考虑在应用程序的交互部分中引入一些繁重的工作。我对数值工作的首选语言仍然是 Fortran,因为它易于实现带掩码的多维数组。

此外,虽然我并不是直接对我的源代码本身进行批评,但如果有人能发现我可以合并的任何明显优化,我将不胜感激。

函数timeebo 是测试函数,我在SQL Developer 中用一个简单的select timeebo from dual; 调用它。

create or replace FUNCTION gammln(
    x IN binary_double)
  RETURN binary_double
IS
  --Lanczos' approximation to the Log Gamma function
  gammln binary_double;
  ser binary_double;
  tmp binary_double;
BEGIN
  tmp := x + 5.5;
  tmp :=(x + 0.5) * Ln(tmp) - tmp;
  ser := 1.000000000190015;
  ser := ser + 76.18009172947146 /(x + 1.0) ;
  ser := ser - 86.50532032941677 /(x + 2.0) ;
  ser := ser + 24.01409824083091 /(x + 3.0) ;
  ser := ser - 1.231739572450155 /(x + 4.0) ;
  ser := ser + 1.208650973866179E-03 /(x + 5.0) ;
  ser := ser - 5.395239384953E-06 /(x + 6.0) ;
  RETURN tmp + Ln(2.5066282746310005 * ser / x) ;
END;
/
CREATE OR REPLACE FUNCTION PoissonDist(
    k      IN INTEGER,
    lambda IN binary_double)
  RETURN binary_double
IS
BEGIN
  RETURN Exp((k * Ln(lambda)) - lambda - gammln(k + 1)) ;
END;
/
CREATE OR REPLACE FUNCTION EBO(
    stock    IN pls_integer,
    pipeline IN binary_double,
    accuracy IN binary_double DEFAULT 0.0000000001)
  RETURN binary_double
IS
  i pls_integer;
  EBO binary_double;
  term binary_double;
  temp binary_double;
  PoissonVal binary_double;
  peaked BOOLEAN; --Flag the Poisson curve as having peaked
BEGIN
  EBO        := 0.0;
  IF(pipeline = 0.0) THEN
    RETURN EBO;
  END IF;
--Initialise
i            := 1;
peaked       := false;
PoissonVal   := PoissonDist(stock + 1, pipeline) ;         --Get p() value
IF(PoissonVal < accuracy AND floor(pipeline) > stock) THEN --If p() is very
  -- small...
  i          := floor(pipeline)   - stock;         --Revise i to just below peak of Poisson curve
  PoissonVal := PoissonDist(stock + i, pipeline) ; --Get p() value close to
  -- peak
  temp := PoissonVal *(pipeline / CAST(stock + i + 1 AS binary_double)) ; --
  -- Store poisson value just above peak
  LOOP
    term       := CAST(i AS binary_double) * PoissonVal;
    EBO        := EBO                         + term;
    i          := i                           - 1; --Work backwards
    PoissonVal := PoissonVal                  *(CAST(stock + i + 1 AS DOUBLE
    PRECISION)                                / pipeline) ; --Revise Poisson
    -- value for next time
    EXIT
  WHEN(term < accuracy OR i = 0) ;
  END LOOP;
  i          := 1 + floor(pipeline) - stock;
  PoissonVal := temp;
  peaked     := true;
END IF;
LOOP
  term       := CAST(i AS binary_double) * PoissonVal;
  EBO        := EBO                         + term;
  i          := i                           + 1;
  PoissonVal := PoissonVal                  *(pipeline / CAST(stock + i AS
  binary_double)) ; --Revise Poisson value for next time
  IF(CAST(stock + i AS binary_double) > pipeline) THEN
    peaked                              := true;
  END IF;
  EXIT
WHEN(term < accuracy AND peaked) ;
END LOOP;
IF(EBO < accuracy) THEN
  EBO := 0.0;
END IF;
RETURN EBO;
END;
/
CREATE OR REPLACE FUNCTION timeebo
  RETURN binary_double
IS
  i pls_integer;
  EBOVal binary_double;
  acc binary_double;
BEGIN
  acc := 0.0;
  FOR i IN 1..1000000
  LOOP
    EBOVal := EBO(500, CAST(i AS binary_double) / 1000.0) ;
    acc    := acc                                + EBOVal;
  END LOOP;
RETURN acc;
END;

【问题讨论】:

请问你为什么选择plsql来完成这个数字任务? 因为应用程序的其他 95+% 是用 Oracle APEX 编写的,所以数字代码需要访问数据库中的数据,并且还需要将其结果插入回数据库中。 对于 Oracle 数据库中的数字内容,Java 会是更好的选择吗?如果将数据从 Oracle 类型移动到 Java 类型(反之亦然)的开销不占总执行时间,那么 Java 可能比 PL/SQL 更快。 在你的代码中没有我注意到的数据库访问:) @WilliamRobertson - 回复:我用作循环计数器的整数阻止它成为 simple_integer - 有一个简单的解决方案,使用 WHILE 循环而不是 FOR 循环。 【参考方案1】:

这不是 OP 问题的答案(但它是一个“概念证明”,展示了他在 PL/SQL 中所做的事情如何可以在普通 SQL 中完成)。我只使用答案格式,因为我将在下面做的内容不适合评论。

OP 要求查看如何在纯 SQL 中将无限级数求和到所需的精度。我举两个例子。

第一个例子

具有正项的无限级数:e = sum [ j = 0 to infinity ] ( 1 / 阶乘(j) )。从 0 到 n 之和的误差的一个微不足道的上限是添加到系列中的最后一项。下面的递归查询(它需要 Oracle 11.1 或更高版本 - 实际上是 11.2 我编写它的方式,在声明中使用列名,但它可以很容易地为 11.1 更改)计算 e 的值精确到38 位小数(Oracle 中可用的最大精度)。 e 的反阶乘级数收敛很快;这只需 35 步,在我的旧的家用电脑上运行不到 0.001 秒(这只是一台带键盘的大型戴尔平板电脑)。

编辑:呃!只有我可以发布 e = 3.71828 的内容!即使在递归查询中我添加了所有项(包括 1/0!),我还是从 1 而不是 0 开始求和。(现在更正,但在更正之前有这个错误。)

with 
     rec ( j, s, next_term, err ) as (
       select  0, 0, 1, 2
         from  dual
       union all
       select  j+1, s + next_term, next_term/(j+1), next_term
         from  rec
         where err > power(10, -38) and j < 1000000000
     )
select max(j) as steps, round(max(s), 38) as e 
from   rec
;

STEPS                                         E
-----  ----------------------------------------
   35  2.71828182845904523536028747135266249776

第二个例子

好的,现在让我们采用一个交替序列(其中最后一项的绝对值始终是误差的上限),让我们采用一个收敛速度非常慢的序列:

ln( 2 ) = sum [ j = 1 infinity ] ( (-1)^(j - 1) / j )

下面的查询计算ln(2),精确到小数点后五位;在这里我们事先知道我们需要 100,000 步,而我的机器上的计算大约需要 1.1 秒。 (请记住,这是一个非常缓慢融合的系列。)

with
     rec ( j, s, sgn ) as (
       select  0, 0, 1
         from  dual
       union all
       select  j+1, s + sgn / (j+1), -sgn
         from  rec
         where j <= 100000
     )
select 100000 as steps, round(s, 5) as ln_2
from   rec
where  j = 100000
;

 STEPS     LN_2
------  -------
100000  0.69314

【讨论】:

非常有用,谢谢。我不知道with 可能是自引用的。 响应您的编辑,我只是​​假设您正在计算 e + 1。 @MattWenham - 不,我以一种方式编写代码,我不喜欢它,我简化了它并使其更合乎逻辑,但我忘记更改初始化。这可能发生。但是 3.71828 正盯着我的脸,我看了好几遍还是没看明白,这不应该发生。 :-) 仅供参考:递归 with 子句到达 Oracle 11gR2。但是现在每个人都应该使用 12c,所以没关系:) @APC - “现在每个人都应该使用 12c”。是的,你说...我们的客户今年才迁移到 11gR2,现在正在升级到 APEX 5。【参考方案2】:

根据上述cmets中的建议总结我的发现:

消除铸件对建议的改进产生了最大的影响。在binary_double 中执行所有操作可使代码运行速度提高约 3.5 倍,超过十秒。然后应用 simple_doublepragma_inline 将其平均缩短到 9.8 秒左右。

应用了上述更改的我的代码的当前版本如下。 PoissonDist 函数可以进一步优化,但最初的问题更多是关于如何使现有代码运行得更快,而不是代码本身的优化。

编辑:代码已被修改以反映pragma_inline 的正确使用,这已将执行时间减少到大约 9.5 秒。

create or replace FUNCTION gammln(
    x IN simple_double)
  RETURN simple_double
IS
  --Lanczos' approximation to the Log Gamma function
  ser simple_double := 1.000000000190015d;
  tmp simple_double := x + 5.5d;
BEGIN
  tmp :=(x + 0.5d) * Ln(tmp) - tmp;
  ser := ser + 76.18009172947146d /(x + 1.0d) ;
  ser := ser - 86.50532032941677d /(x + 2.0d) ;
  ser := ser + 24.01409824083091d /(x + 3.0d) ;
  ser := ser - 1.231739572450155d /(x + 4.0d) ;
  ser := ser + 1.208650973866179E-03d /(x + 5.0d) ;
  ser := ser - 5.395239384953E-06d /(x + 6.0d) ;
  RETURN tmp + Ln(2.5066282746310005d * ser / x) ;
END;
/
create or replace FUNCTION PoissonDist(
    k      IN simple_double,
    lambda IN simple_double)
  RETURN simple_double
IS
BEGIN
  PRAGMA INLINE (gammln, 'YES');
  RETURN Exp((k * Ln(lambda)) - lambda - gammln(k + 1d)) ;
END;
/
CREATE OR REPLACE FUNCTION EBO(
    stock    IN simple_double,
    pipeline IN simple_double,
    accuracy IN simple_double DEFAULT 0.0000000001)
  RETURN simple_double
IS
  i simple_double          := 1d;
  EBO simple_double        := 0d;
  term simple_double       := 0d;
  temp simple_double       := 0d;
  PRAGMA INLINE(PoissonDist, 'YES') ;
  PoissonVal simple_double := PoissonDist(stock + 1d, pipeline) ;
  peaked BOOLEAN           := false; --Flag the Poisson curve as having peaked
BEGIN
  IF(pipeline = 0.0d) THEN
    RETURN EBO;
  END IF;
IF(PoissonVal < accuracy AND floor(pipeline) > stock) THEN --If p() is very
  -- small...
  i          := floor(pipeline)   - stock;         --Revise i to just below peak of Poisson curve
  PRAGMA INLINE(PoissonDist, 'YES') ;
  PoissonVal := PoissonDist(stock + i, pipeline) ; --Get p() value close to
  -- peak
  temp := PoissonVal *(pipeline /(stock + i + 1d)) ; --
  -- Store poisson value just above peak
  LOOP
    term       := i          * PoissonVal;
    EBO        := EBO        + term;
    i          := i          - 1d;                           --Work backwards
    PoissonVal := PoissonVal *((stock + i + 1) / pipeline) ; --Revise Poisson
    -- value for next time
    EXIT
  WHEN(term < accuracy OR i = 0d) ;
  END LOOP;
  i          := 1d + floor(pipeline) - stock;
  PoissonVal := temp;
  peaked     := true;
END IF;
LOOP
  term       := i          * PoissonVal;
  EBO        := EBO        + term;
  i          := i          + 1d;
  PoissonVal := PoissonVal *(pipeline /(stock + i)) ; --Revise Poisson value
  -- for next time
  IF((stock + i) > pipeline) THEN
    peaked      := true;
  END IF;
  EXIT
WHEN(term < accuracy AND peaked) ;
END LOOP;
IF(EBO < accuracy) THEN
  EBO := 0.0d;
END IF;
RETURN EBO;
END;
/
create or replace FUNCTION timeebo
  RETURN binary_double
IS
  i binary_double;
  EBOVal binary_double;
  acc binary_double;
BEGIN
  acc := 0.0d;
  FOR i IN 1d..1000000d
  LOOP
    PRAGMA INLINE (EBO, 'YES');
    EBOVal := EBO(500d, i / 1000d) ;
    acc    := acc                                + EBOVal;
  END LOOP;
RETURN acc;
END;

【讨论】:

感谢您的反馈。看到最终版本可能会很有趣。 上面添加的当前版本的代码,感谢您的关注。 我认为pragma inline 不会做任何事情,除非它就在通话之前。 (如果您使用alter session set plsql_warnings = 'ENABLE:ALL', 'DISABLE:5018'; 或通过您的工具首选项设置启用编译器警告,您可能会看到类似PLW-05011: pragma INLINE for procedure 'GAMMLN' does not apply to any calls 的警告,当您将其移至PLW-06004: inlining of call of procedure 'GAMMLN' requested 时会发生变化。)或者只需设置plsql_optimizer_level = 3 并使其自动内联所有内容。 @WilliamRobertson - 重新阅读文档后,您似乎是对的......这很奇怪,因为上述更改似乎确实缩短了执行时间。 我很想知道你修复它后得到了多少改进。

以上是关于PL/SQL 的数值优化或替代方案的主要内容,如果未能解决你的问题,请参考以下文章

PL/SQL 组函数的替代方案

获取 PL/SQL:数字或值错误:字符到数字的转换错误

PL/SQL - 防止 ORA-06502

如何确保在编译 PL/SQL 程序时启用了优化?

如何在Haskell中优化数值库的速度

PL/SQL 02 声明变量 declare