企业应用系统,尤其是金融保险行业的应用系统,常常会涉及大量繁杂的计算。例如,在保险行业,客户购买了一份投连保险产品,系统要为客户展示未来数十年的利益。这些利益按月计算,按年累计,各项金额前后关联,计算形态多种多样。这类保险产品的利益展示可能涉及万千次的计算。如果这些计算没有经过良好的设计,系统的性能常常会变得低效。
计算是软件行业中的一个重要领域。对于企业应用系统来说,计算不仅包含了算法,还包含了一套应用算法的框架。仍以保险行业为例,保险产品形形色色,计算规则、计算公式、计算过程千差万别。计算框架要有能力对这些看似繁杂无序的内容做出归纳和抽象,以最小的代价容纳它们,并使之享受到算法的好处。
值得强调的是,计算框架和算法是计算领域中两个不同的子领域。这两个子领域的目标和内容都是不同的。计算框架试图容纳变化,并使之用到算法的好处;算法则是基于一套数据结构,在空间和速度的权衡下进行高效计算。清晰的领域划分可以使我们更加专注在领域内的工作,而模糊的领域认识,常常会使我们迷失方向,做出错误的选择。在实践中,我们常常看到计算框架和算法之间没有明显的边界,算法中嵌入计算框架的元素,这使得算法无法进行充分优化。
最近,我正在设计一个计算服务。面对数百页各类产品的业务计算说明,从中找到计算特征,提炼高效的算法,并设计一个框架来容纳各种变化,这真的是一次有趣的挑战。由于具体的业务内容不适合公开展示,因此,本文将只是着眼于计算设计的方法和高度抽象的算法模型。
了解业务领域,熟悉业务需求,当然是软件开发的前提。接下来,我们需要从繁杂的业务描述中找到计算特征。我不知道这一步是否有可供操作的方法,事实上,我发现这似乎是经验积累后的一种转化。当你阅读需求的时候,很多工作已经在头脑中自然地展开,识别、判断、证实、归类、忽略、抽象,一些想法渐渐从模糊到清晰。好吧,我得到了未经处理的计算特征。见图1。
从图1可知,一组计算子(通过计算获得,计算形态不限)将作为输入块(A)。这些计算子之间存在依赖关系,即某些计算子通过其他的计算子参与才能完成计算。这种依赖关系可能是多级的,例如,计算子1依赖于计算子2,计算子2依赖于计算子3。
这一组计算子被送入下方紧邻的一个计算块(B),这个计算块(B)内部的计算子之间不仅存在输入块(A)中的计算子规则,还可能会依赖输入块(A)中的计算子。
当前的计算块(B)会被送入其下方紧邻的另一个计算块(B'),计算块(B')不仅像计算块(B)一样,依赖于输入块(A),还依赖于计算块(B)。
计算块(B)还会被送入其右方紧邻的计算块(C),计算块(C)和计算块(B’)类似,依赖于输入块(A)和计算块(B)。
计算块(C)会被送入其下方紧邻的计算块(C’),计算块(C’)依赖于输入块(A),计算块(C),计算块(B’)。
上述这种规则将沿着Y轴向右方延伸,沿着X轴向下方延伸。
最终,数据将到达输出块(O)。输出块(O)与输入块(A)及紧邻其上方的一排计算块有依赖关系。
上述计算特征表明,这类计算的难点在于计算子之间的依赖关系。这些繁杂的依赖关系如果没有经过清晰的梳理,会使你不得不保存大量的历史数据,并不得不进行串行化的低效计算。失控的大空间,以及闲置的CPU计算能力,对于算法来说都是致命的缺陷。
在总结出计算特征后,我们将开始算法设计。算法设计通常以硬件资源为基础(一台40核的服务器可供使用)。我们采用了并行计算,并确立了一个基本原则,即以最大限度并行计算为优先,换句话说,不是以业务逻辑为优先。这个基本原则要求我们必须在更高层次上进行抽象,确保只有在无法避免时才进入串行计算。显然,由于计算特征中的依赖关系,我们很难完全避免这一点。不过,从另一个角度来看,这种缺陷未尝不是一件好事,完全占用CPU会导致其他的严重问题,我们必须留出CPU的计算能力给其他的并发请求。现在,我们的并行计算模型像是一个锯齿形,见图2,对比了串行计算和锯齿形并行计算。
算法设计和业务领域的设计有些类似,两者都需要对数据(结构)进行建模,并基于数据模型进行逻辑处理。不同之处在于,算法显然更偏向于求值计算,而且更加关注指令的简洁和内存空间的使用。任何设计都不是一蹴而就,你需要在头脑中设想计算的过程,并不断迭代,最终提炼出恰当的概念来容纳你对于计算的描述。事实上,我同样不知道该如何针对这个过程给出可操作的具体方法。总之,我构思了这些概念,见图3。
我们将有一个计算的上下文(Calculation Context),这个上下文包含了输入(Input),输出(Output),以及一组计算布局(Calculation Layout)。每一个计算布局都可以迭代执行,执行的结果会进入计算布局栈区(Calculation Layout Stack Area),栈区的数据来自于并行计算区(Calculation Parallel Area),由于并行计算,栈区的数据是无序的,需要经过后续处理,进入计算布局输出(Calculation Layout Output)。一个计算布局包含了多个有序的并行计算区。每一个计算区又包含了多个计算分组(Calculation Item Group),这些计算分组是有序的,而每个计算分组又包含了多个计算子(Calculation Item),这些计算子是无序的。理论上,无序的分组都是可以进行并行计算的。
当有了这套数据模型后,我们的算法实现就变得相对简单了。当然,在算法实现过程中,仍然有很多算法领域内的问题需要给出答案。例如,如何保证尽可能少地占用内存空间?如何采用最合适的基本数据结构?如何使指令变得最大限度的精简?