“任何人都可以用海豚解决灰色悖论,而您尝试不使用海豚来解决。 ”

实际上,我计划以一个稍微不同的方式度过一个周末,去直升机哈克(不是我是直升机的粉丝,只是为了看看年轻人在发明什么,像这样闲逛),但是姐姐却坚决反对。 当然,我坚持(也就是说,我咯咯笑了几声,说:“嗯,也许……还是很有趣的。”),但是她很随和,当我的妻子站在她的身边时,就没有旅行的机会。 好吧,好的,“我不是真的想要它”,但是我对编程领域的一个有趣难题感到有些疑惑,我正在为自己思考,我正在报告这个问题。
(需要注意的是-上个周末一直是这样-编写程序需要几个小时,编写一份报告,并且五天的公共交通工具旅行尚未完成。)
在最近的一篇文章中,作者考虑了(除其他事项外)在参数相对较弱的MK上加速Bezier曲线(KB)计算的问题。 好吧,实际上,这些参数处于70年代平均大型机的水平,但是目前认为这显然不足。 我认为,由于采取了某些措施,作者设法设法加快了计算速度,但这显然是不够的,因此,我决定写出应该如何做作为第一近似值。 我完全知道解决性能问题的通用方法-以更高的频率使用MK或改用其他产品,但是我来自于我们研究它的成本,仅仅是因为一无所有。 目前,这种方法已经过时了,但是在我看来,这对现代的哈勃读者来说并不是没有兴趣的。
我们解决这个问题-我们想尽快计算由极点A和B和虚点C.定义的Bezier曲线上的点的坐标。
其中T从0到1(含0)变化。 (在Wiki上,他们一次写出这个公式是秘密的,那样很奇怪,但是一切皆有可能)。 显然,我们不会采用复杂的形式,而是分别搜索X和Y坐标,只需计算此表达式中算术运算符的数量-7次乘法和5次加法(=> 7 * 5 + ) 一个好的编译器(现在所有的编译器都是好的,并且如果您没有明确禁止它们的话会进行优化)可能会将成本降低到7 * 3 +,尽管最好通过预先计算(1-T)来帮助他。 一般来说,一个好的编译器通常可以解决公式中的所有值是否都由常量表示的问题,但是我们假定所有值在静态上都是未定义的。
第一部分,数学
我们开始优化过程,为此我们将方括号括起来并在T处将其分组(也许有一天,编译器将能够为我们执行此操作,但到目前为止,这部分工作已分配给自然智能),
=> 5 * 5 +,明显优于7 * 5 +的初始值,但仍应考虑相对改善的7 * 3 +。
如果我们把完成加法运算的时间花在一个时间上,那么完成乘法的时间通常将不少于一个,通常是更长,但是多少取决于MK的实现(首先写-在体系结构上,但这不是完全正确的)。 当晶体上没有硬件乘法器时,乘法执行时间将是原来的十倍(30+)倍,如果存在,它将是原来的几倍(1-6)。 因此,我们可以有把握地相信,用加法代替乘法运算几乎总是会增加执行时间(通常很重要)。 好吧,我们会立即注意到,从定点数到浮点的过渡(我们不去证明这一事实)会导致执行时间增加20倍以上(加法在这里很有影响),但乘法只会稍微增加一点。 因此,对于浮点数,加法和乘法时间相差不大,尤其是在相对方面(我们可以预期最大为2倍),但是它们仍然有所不同并且不赞成乘法,因此这里有一个好处。
回到上一段,我们发现对于PT而言5 * 5 +评级不应该比7 * 3 +具有明显的优势,但是我们仍有储备。 请注意以下事实:当参数T更改并且曲线的所有其他参数都是固定的(但不是常数,而是可惜)时,我们必须计算贝塞尔曲线上的点的值集,然后可以预先计算公式的其余部分并得出
=> 3 * 2 +,其中
和
已经很好了,但是如果您想起霍纳的计划并写
=> 2 * 2 +,那么与“额头上”的决定相比,我们必须赢取2次以上,几乎是3次,而且这些优化是显而易见的。
让我们通过实践检查一下理论(尽管这是完全多余的,但我们对我们的估计充满信心,但是突然间我低估了编译器),为此我们需要测量在实际硬件上执行不同选项的实时性。 好吧,碰巧的是,我在家里有很多来自各种公司的MK调试板(包括稀有的Luminary Micro或Intel Edisson调试,现在尝试购买),但是没有一块Arduino板(“好我们没有菠萝”)。 这似乎是一个死胡同,但是有很多选择-一个非常有趣的站点tinkercad.com会为我们提供帮助,您可以在该站点上使用Arduino模块在面包板上构建电路,编写草图并立即执行。 同时,您可以设置断点,逐步执行程序,甚至(发生故障时,对于真正的Arduino而言,这都是前所未有的)。
我们转到此站点开始测量。 首先,我们检查关于操作执行时间的假设,从环境中清除后,我们获得以下整数数据:
8 + 8 => 8-1拍,16 + 16 => 16-2
8 * 8 => 16-2,16 * 16 => 16-14(唯一出乎意料的事情,我以为得到4 * 2 + 4 * 2 = 16,有一些有趣的优化方法),
8/8 => 8-230,16/16 => 16-230。
请注意最后两位数字,从它们很明显,如果我们真的想快速计数,则禁止除法运算。 现在(最终),我们测量使用24位尾数对PT数执行操作所需的时间
a + b-126(并在很大程度上取决于操作数),a * b-140,a / b-482。
获得的数据与我们的理论假设很好地相关,很明显,此MK上有一个硬件实现:用于乘法,除法,而不用于运算PT。
现在我们开始测量完整计算的时间。 我们设置值A = 140,B = 120,C = 70,并在设计局上建立170个均匀分布的点。 为什么要精确指定这些值-在评估性能时在指定的职位中给出了这些值。 以下是算法和相应的测试执行时间。
公式(1)=> 20ms或每个样本1,900个时钟周期
公式(1)=>每个样本18ms或1660个时钟周期(单独考虑1-T)
公式(2)=>每个样本16ms或1540个时钟周期
公式(3)=> 10ms或每个样本923个时钟周期
公式(4)=> 8ms或每计数762次
可以看出,执行时间的减少(从20ms减少到8ms)与预期的执行时间紧密相关,并且我们能够将计算速度提高2倍以上。 请注意,除了完全显而易见的考虑因素和数学之外,我们并不需要其他东西。
现在让我们谈谈如果结果不足的话该怎么办,并且我们已经从计算公式中挤出了所有内容。 他们在这里给我写信(在另一篇文章的评论中),通常可以将任何问题简化为使用FT进行计算,尽管该假设存在明显争议(尝试对Navier-Stokes方程的数值解法这样做),但在这种特殊情况下,此建议适用尽管像往常一样存在细微差别。
第二部分,计算
对算法的修改用完后,仅保留数据结构,我们输入定点数的土壤。 在这里,我们会发现许多我们在PT时没有想到的陷阱-范围和准确性(通常,对于PT,人们应该考虑这些问题,但是这里的一切都比较简单,为我们做了很多工作)。 有必要对该问题进行小规模研究,以确定FT的必要表示形式(在前面的9.7中选择,根据结果判断,这显然是不够的),但我建议采取略有不同的方法。 顺便说一句,如果我们不在该间隔上执行170个步骤,而是执行128个步骤(我认为没有理由禁止我们执行此步骤),那么此想法将非常适合我们。 如果考虑到定义KB的常量由整数给出的事实,并且唯一的参数T可以由形式和/的一部分表示,并且我们将使用结果在屏幕上进行渲染,即转换为整数坐标,那么我们可以只需以整数完成所有操作即可,处理速度更快。
我们仅使用最后一个公式并将其重写为新的符号
(=> 2 * 2 + 2 /),其中A1和B1的计算方法与PT相同。 显然,所有数字都是整数,并且应该更快地执行相应的操作。 为了在整数除法(2/3 = 1!= 1.5)的运算过程中不会损失精度,并在最后时刻进行除法,我们将公式稍微转换为以下形式:
(=> 4 * 2 + 2 /)。 所有FT数,因此我们实现此算法并获得...这里,祖母,还有Yuryev的日子... 1869个周期,但是这比FT差得多,我们从这种情况开始,有些垃圾,因为整数要快得多。
我们开始汇报,结果发现仅仅改变变量的类型是不够的。 首先,我们必须使用不是8甚至16的数字,而是32位,否则将发生溢出,并且数字长,虽然比PT快,但不足以弥补算法中的缺陷。我们再次对每个度量计算了常数-我们通过初步计算将其删除B2 = B1 * I,A2 = A * I *I。 然后我们得到
(=> 2 * 2 + 2 /)的结果为1684,比上一个更好,但是我们仍然没有摆脱它。
我们排除了另一个常数And2 = And *的计算
(=> 2 * 2 +1 /),执行时间为956个周期-但这很有趣,排除一项操作导致生产率显着提高。
那就是使我们放慢速度的原因-除法,因为这是一个非常耗时的操作,但是我们有一个有趣的技巧可以解决它。 要计算表达式1 /,我们可以进行基本变换1 /= 1 /*(/)= 1 *(/)/。 如果我们选择2的度数作为H,则除以H可以用移位代替,如果指数是8的倍数,则不需要移位。 N / A的值必须诚实地计算,但只能计算一次,此后在计算周期中仅保留乘法。
请注意,我们进行的转换不太正确,并用取整后的值K替换了N / A,以专门用整数进行运算。 不正确之处在于准确性的损失,应进行进一步的研究以证明这种方法在我们的案例中的适用性。 我们以(K * I + d)/ I = K +(d / I)的形式写H / I,其中q小于I。那么从H / I到K的绝对误差为d / I,相对误差为d / I I /(K + d / I)> = d / I /(K + 1)〜d / I / K,前提是K >> 1(这不是移位)。 因此,由于绝对计算误差等于A * d / I / K> = A * 1 / N / I,因此应选择尽可能大的H值。 如果我们希望误差不大于1,则必须承受条件A / K <= 1,然后K> = A,我们将K * I> = A * I转换,这意味着H> = A * I,那么我们就不会准确性下降。 在我们的情况下,A <= 256和I <= 256,我们得到H> = 2 ** 16,这是完全可以接受的。 显然,在以上公式中,应使用原始编号的模块。
对于未来,我们注意到,如果不舍入而是取整为最接近的整数,则要求会有所降低,并且尽管有细微差别,H应该足够大。
无论如何,我们都可以提供所需的精度,并获得以下算法:H = 2 ** 16; K = [N / A](I <256); 0 <=和<= AND;
(=> 4 * 2 + 2 >> 16),其中所有操作均对长整数执行。 我们实现了该算法,并获得了583个时钟周期...但是这已经接近理想状态,但还不是理想状态。
接下来是特定MK的小设置-使用全局变量更快。 比本地时钟要快,但寄存器本地时钟甚至更快,这可以将时间减少到506个时钟周期。
此外,我们注意到,移位之前的最后一个乘法可以用16位数字执行,这将产生504-个小问题,但是很好。
总体而言,与1900/504中的“前额”实施相比,我们加快了计算速度-超过了3倍,而且我们完全没有丢失这个词。 这是我称时间优化的结果,而不是原始帖子中收到的20%。
是否有可能实现甚至更好的指标-这是可能的,但这是下一篇文章的主题。