“计算操作取决于函数的实现方式”实际上意味着什么?

时间:2017-05-20 16:46:54

标签: performance

我一直在研究算法效率,课程的一部分说计算操作的数量(而不是算法的计时)取决于函数的实现方式(写作是一个缺点),它也取决于算法(写作上行)。

这究竟是什么意思?这两段代码是同一算法的两个不同实现是什么意思(我在这里遗漏了一些细微之处,或者它只是意味着两个函数做同样的事情但语法略有不同,算作两个独立的同一算法的实现)?事实上,它取决于算法是否合适,但事实上它取决于实现不好?

2 个答案:

答案 0 :(得分:1)

不能代表课程作者的意思,但也许我可以解决你的第二个问题。

算法是为了实现某个目标/计算所需的动作的描述。它最常用数学语言给出。计算机程序是实现算法[1]的一种方式,也是最常见的。即使它们是相当抽象的东西,它们仍然比数学描述更具体。它们与编写的语言和环境有关,它有各种怪癖,你试图解决问题的具体细节[2],甚至是编写它的特定工程师。因此,实现某种算法的两个程序或程序部分是不同的,甚至具有不同的性能属性是很自然的。对于某个输入执行的指令数肯定属于两个实现之间不同的属性桶。因此。

[1]另一种方式可能是硬件,如数字电路或模拟计算机,或通过某些机械过程,如钟表或19世纪的机械自动机之一,甚至某些生物或化学过程。 [2]为了澄清,通用排序例程可能以不同于16位整数排序例程的方式编写,即使它们都实现了QuickSort。

答案 1 :(得分:1)

两者都不正确,事实就在中间。算法不是30年以上的任何东西,但今天编译器可以解构你的算法并以不同的方式重建它(如果它已被编程识别你想要做的事情)。

从数学上讲:你可能听说小学里有一个关于添加1到100的所有数字,从0到100更容易,所以这是99或100个加法操作是吗?加上一个循环,它是一个计数器和一个比较。那么如果你意识到0 + 100 = 100,99 + 1 = 100,98 + 2 = 100会怎样。有50对可以加起来100,然后剩下50对。因此我们可以减少100次添加和一次100次添加循环,并比较低至50 * 100 + 50或50 * 101。一次乘法。您可能可能会制作一个具有一些约束的算法,但是将所有数字从0加到N,其中N为正数作为约束,即使对N的奇数值也会产生不同的通用算​​法,也许不是,可能有N / 2在那里和一些乘法,也许一个加。比循环变量必须进行多次添加和比较的循环中的N添加要便宜得多。

但实施情况如何:

00000000 <fun1>:
   0:   e59f0000    ldr r0, [pc]    ; 8 <fun1+0x8>
   4:   e12fff1e    bx  lr
   8:   000013ba            ; <UNDEFINED> instruction: 0x000013ba

0000000c <fun2>:
   c:   e59f0000    ldr r0, [pc]    ; 14 <fun2+0x8>
  10:   e12fff1e    bx  lr
  14:   000013ba            ; <UNDEFINED> instruction: 0x000013ba

00000018 <fun3>:
  18:   e59f0000    ldr r0, [pc]    ; 20 <fun3+0x8>
  1c:   e12fff1e    bx  lr
  20:   d783574e    strle   r5, [r3, lr, asr #14]

算法在这种情况下无关紧要,注意编译器甚至将伪随机求和循环减少到答案中。

unsigned int fun1 ( unsigned int x )
{
    return(x*10);
}
unsigned int fun2 ( unsigned int x )
{

    return((x<<3)+(x<<1));
}
unsigned int fun3 ( unsigned int x )
{
    return(((x<<2)+x)<<1);
}

我希望有一个倍增但当然没有得到一个,也许我需要指定cpu。

00000000 <fun1>:
   0:   e0800100    add r0, r0, r0, lsl #2
   4:   e1a00080    lsl r0, r0, #1
   8:   e12fff1e    bx  lr

0000000c <fun2>:
   c:   e1a03080    lsl r3, r0, #1
  10:   e0830180    add r0, r3, r0, lsl #3
  14:   e12fff1e    bx  lr

00000018 <fun3>:
  18:   e0800100    add r0, r0, r0, lsl #2
  1c:   e1a00080    lsl r0, r0, #1
  20:   e12fff1e    bx  lr

它不需要识别fun2而其他的是相同的。我已经看到mips后端实际上调用了另一个中途,所以fun3会在这种情况下分支到地址0,例如,这比仅运行指令更昂贵,并没有为我做这个,所以也许我需要一个更复杂的功能

现在假设x是偶数

unsigned int fun1 ( unsigned int x )
{
    unsigned int ra;
    unsigned int rb;
    rb=0;
    for(ra=0;ra<=x;ra++) rb+=ra;
    return(rb);
}

unsigned int fun2 ( unsigned int x )
{
    return((x/2)*(x+1));
}

我们应该得到不同的结果,编译器不那么聪明......

00000000 <fun1>:
   0:   e3a02000    mov r2, #0
   4:   e1a03002    mov r3, r2
   8:   e0822003    add r2, r2, r3
   c:   e2833001    add r3, r3, #1
  10:   e1500003    cmp r0, r3
  14:   2afffffb    bcs 8 <fun1+0x8>
  18:   e1a00002    mov r0, r2
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e1a030a0    lsr r3, r0, #1
  24:   e2802001    add r2, r0, #1
  28:   e0000293    mul r0, r3, r2
  2c:   e12fff1e    bx  lr

我们假设乘法是便宜的,docs会说一个时钟,但这不一定是真的,有一个管道他们可以通过消耗更多并将时间埋在管道中,或者如你所见,可以节省大量的芯片空间在非流水线处理器中,乘法的时钟更长。我们可以假设它埋在管道中,如果你能保持管道平稳移动,那真的很快。

无论如何,我们可以安全地假设在最后一个例子中,加法循环比优化算法慢得多。所以算法和实现在这里帮助我们。

unsigned int fun1 ( unsigned int x )
{
    return(x/10);
}

00000000 <fun1>:
   0:   e59f3008    ldr r3, [pc, #8]    ; 10 <fun1+0x10>
   4:   e0821390    umull   r1, r2, r0, r3
   8:   e1a001a2    lsr r0, r2, #3
   c:   e12fff1e    bx  lr
  10:   cccccccd    stclgt  12, cr12, [r12], {205}  ; 0xcd

这是一个有趣的我可以/已经表明,如果你的处理器有分频,乘以1/5或1/10解比慢直线分频,还有额外的负载有转换以及乘法,其中除法可能是负荷和除法。你必须让内存变慢,以便额外的负载和额外的提取吞噬差异,这里再次分割一般较慢。但是编译器在大多数情况下仍然是正确的乘法更快,所以这个解决方案没问题。但它没有实现我们直接要求的操作,因此算法从期望变为其他。实现保存了算法,或者至少没有伤害它。

查找FFT,这是一个典型的例子,从具有一定数学量的基本算法开始,您可以计算操作,然后通过各种方式重新排列数据/操作以减少数学,并进一步减少它。这很好,在这种情况下,你很可能会帮助编译器。但是如果你允许的话,实现可能会有所帮助,特别是如何编写代码可以采用一种很好的算法并使其变得更糟。

unsigned int fun1 ( unsigned int x )
{
    return(x*10.0);
}

00000000 <fun1>:
   0:   ee070a90    vmov    s15, r0
   4:   ed9f6b05    vldr    d6, [pc, #20]   ; 20 <fun1+0x20>
   8:   eeb87b67    vcvt.f64.u32    d7, s15
   c:   ee277b06    vmul.f64    d7, d7, d6
  10:   eefc7bc7    vcvt.u32.f64    s15, d7
  14:   ee170a90    vmov    r0, s15
  18:   e12fff1e    bx  lr
  1c:   e1a00000    nop         ; (mov r0, r0)
  20:   00000000    andeq   r0, r0, r0
  24:   40240000    eormi   r0, r4, r0

unsigned int fun1 ( unsigned int x )
{
    return(x*10.0F);
}

00000000 <fun1>:
   0:   ee070a90    vmov    s15, r0
   4:   ed9f7a04    vldr    s14, [pc, #16]  ; 1c <fun1+0x1c>
   8:   eef87a67    vcvt.f32.u32    s15, s15
   c:   ee677a87    vmul.f32    s15, s15, s14
  10:   eefc7ae7    vcvt.u32.f32    s15, s15
  14:   ee170a90    vmov    r0, s15
  18:   e12fff1e    bx  lr
  1c:   41200000            ; <UNDEFINED> instruction: 0x41200000

微妙,需要一个32位常数对64,数学是单对双倍,采取一个更复杂的算法,将加起来。最后我们可以做一个固定点乘法并获得相同的结果吗?

unsigned int fun1 ( unsigned int x )
{
    return((((x<<1)*20)+1)>>1);
}

00000000 <fun1>:
   0:   e0800100    add r0, r0, r0, lsl #2
   4:   e1a00180    lsl r0, r0, #3
   8:   e1a000a0    lsr r0, r0, #1
   c:   e12fff1e    bx  lr

当x是整数时,是否会有任何舍入?

没有任何事实,实现并不重要(即使在一个小黑板和几个宽黑板的教室,或者标记持续时间更长并且擦除同样容易的白板)它是不是算法无关紧要的事实,编程语言并不重要,这不是事实,编译器无关紧要不是事实上编译器选项无关紧要事实并非处理器没有这个事实物质

算法执行的时机不是最终的全部,总而言之,我可以轻松地证明相同的机器代码在相同的处理器和系统上运行得更慢或更快,而无需改变时钟速度等。 对算法进行计时以将错误添加到结果中的方法并不少见。想要快速完成一个系统,时间,调整,时间,调整。有时调整涉及尝试不同的算法。对于一个类似系统的家庭来说,同样的交易,但要了解绩效收益的来源,并根据这些因素在目标家族中的变化情况进行调整。

算法问题是一个事实。实施事项是事实。

请注意,没有理由与你的教授争论,我会称之为事实,通过课程,传递,然后继续。选择你的战斗就像你在现实世界中与老板或同事一样。但是,与现实世界不同,你完成了这个学期,你完成了这个课程,也许永远是教授,现实世界你可能有很长一段时间的那些同事和老板,一场糟糕的战斗或一场失败的战斗可能会影响你很长一段时间。即使你是对的。