是否允许C编译器用另一个算法替换一个算法



例如,您有一个函数sort(int* numbers, size_t count),它有一个bubblesort实现,C编译器可以识别这种模式。是否允许编译器将其更改为另一个示例?就像Quicksort。

另一个例子是将从0到n的所有数字相加,编译器可以用(n*(n-1))/2替换for循环。

如果被调用的函数在优化域内,即使函数是另一个文件,也有一些方法可以做到这一点,那么是的。与在同一文件中拥有被调用函数的方式相同。但这并不意味着编译器根据函数的名称假设被调用代码的真实含义,您可以轻松地创建自己的C库并将其链接进来,并让命名函数(例如qsort)执行任何您想要的操作。所以这是不可取的,但工具做到了:

#include <stdio.h>
void fun ( void )
{
printf("0");
}
Disassembly of section .text:
00000000 <fun>:
0:   e3a00030    mov r0, #48 ; 0x30
4:   eafffffe    b   0 <putchar>

编译器已将所需的函数调用替换为其他一些函数调用。

因此,是的,如果优化器被编程为,可以并且将这样做。如果它具有可见性,则更有可能将两个部分作为一个整体进行优化:

unsigned int more_fun ( unsigned int a, unsigned int b )
{
return(a+b);
}
unsigned int fun ( void )
{
return(more_fun(1,2));
}
Disassembly of section .text:
00000000 <more_fun>:
0:   e0800001    add r0, r0, r1
4:   e12fff1e    bx  lr
00000008 <fun>:
8:   e3a00003    mov r0, #3
c:   e12fff1e    bx  lr

它不是像printf示例那样用另一个函数替换一个函数,而是用可能更优化的内联解决方案替换优化的函数。

当然,一些编译器会优化出一个死循环:

unsigned int fun ( void )
{
unsigned int ra;
for(ra=0;ra<10;ra++)
{
}
return(ra);
}
00000000 <fun>:
0:   e3a0000a    mov r0, #10
4:   e12fff1e    bx  lr

诚然,这是一个微不足道的问题,但我已经生成了伪随机发生器,它们是像这样的死代码,取决于编译器/优化器和代码,如果它已经被编程,它将把循环/代码简化为更简单的代码。

也许这就是你所问的:

unsigned int more_fun ( unsigned int n )
{
return((n*(n-1))/2);
}
unsigned int fun ( void )
{
return(more_fun(10));
}
00000000 <more_fun>:
0:   e2403001    sub r3, r0, #1
4:   e0020093    mul r2, r3, r0
8:   e1a000a2    lsr r0, r2, #1
c:   e12fff1e    bx  lr
00000010 <fun>:
10:   e3a0002d    mov r0, #45 ; 0x2d
14:   e12fff1e    bx  lr

数学尽可能删除。

你不能假设一个特定的编译器会这样做,也不能期望两个编译器产生相同的代码/优化。但是CAN和WILL编译器可以做到这一点,是的,如果它被编程到并可以看到它需要优化的所有代码。

至于";允许";,编译器的工作是用另一种语言在功能上替换一种语言,这在一定程度上就是为什么任何两个从一种语言翻译到另一种(同一输入语言到同一输出目标)的编译器都可以并且将产生不同的输出代码,这是一种功能性的替换。对于一个编译的输出,没有一个正确的答案,它可能会有所不同。因此,在可能的情况下,如上所示,编译器会生成功能等效的代码,这是它的工作,因此它可以完成这项工作。虽然我不认为putchar是printf的替代品,因为编译器没有查看我的printf代码,但同时我认为这个编译器(gcc)有一个命令行选项,不允许这样做。同样,编译器替换一个结构赋值,两个相同类型的结构a=b,也并不罕见;使用memcpy,但同时有一个命令行选项,表示我不想让你用memcpy替换我要求的代码,我希望它单独完成。

在gcc和clang之间,你会看到这些行为,甚至是单词";允许的";去吧,我想这是允许的。如图所示,您可以检查编译器的输出,并查看您的用例,编译器做了什么。

对于优化器识别的更简单的模式,编译器会定期执行此操作。

在您的特定情况下,算法的更改可能会产生适得其反的副作用,因为在大多数排序的数组上,冒泡排序比快速排序更有效。

还要注意的是,优化必须保持相同的语义:用不稳定的排序算法(如快速排序)替换稳定排序算法只有在可以断言不需要稳定性的情况下才是可以接受的。对int的更简单数组进行排序似乎不受此问题的影响,但在具有负零或填充位的体系结构上,这会有所不同。如果数组是浮动类型,排序算法的稳定性肯定会影响最终的顺序。

这里的算法这个词可能有点强,但是的,现代编译器的默认设置通常在当前编译器技术允许的范围内提供更改代码的自由,但同时,几乎每一个编写的现代编译器都为应用程序程序员提供了对编译器能做什么和不能做什么的控制。

在默认设置的情况下,现代编译器能够为简单的算法使用替换构造,在某种程度上,甚至可以指导他们如何根据程序员可设置的偏好选择替换它们。编译器提供了许多优化选项的变体和级别,最常见的是与速度和/或内存使用有关。但更改的范围仅限于更简单的构造,这些构造可以提高效率,也可以完全忽略,具体取决于构造。

在您的特定示例中,对于具有排序算法的函数,算法本身不会被替换,但其中的一些组成部分可以进行优化,具体取决于它们的编码方式。

相反,您也可以将当前编译器设置为,使代码保持原样。例如,在较新版本的GCC(自v4.4起)中,您可以使用以下pragma包围来完全禁用优化:

#pragma GCC push_options
#pragma GCC optimize ("O0")
normal C code
#pragma GCC pop_options

是的,编译器可以也会这样做。例如,考虑使用Kernighan算法的人口计数函数:

int popcount(unsigned x)
{
int c;
for (c = 0; x != 0; x &= x - 1)
c++;
return (c);
}

现代的clang和gcc版本识别这种算法,并给出合适的标志,将其编译为两条指令:

popcount:
popcntl %edi, %eax
retq

这是因为编译器认识到该函数实现了总体计数,并用执行相同目的的机器指令替换整个算法。这不是很整洁吗?

即使编译器能够识别算法,也不应允许用功能等效的算法替换该算法。

原因是选择该算法可能是因为它必须处理的数据的特性,而编译器无法知道这些特性。

请注意,我说的是算法。这比例如计算诸如strlen之类的字符串的长度要多得多。

在排序示例中,如果数据总是几乎排序,那么气泡可能具有最小的开销。尽管泡沫通常效率不高,但它可能是这里效率最高的。

理论上,如果程序的可观察行为相同,编译器可以自由选择程序的执行方式。

编译器技术无法识别如此复杂的模式(工程师甚至没有尝试这样做,这是一个无法确定的问题)——但是的——对于已识别的模式,C标准允许用另一个语义不变的代码替换初始代码。

优化技术针对每个特定优化使用不同的代码表示。复杂的编译器可以用许多不同的中间语言翻译输入代码,并且每种语言的表示可以更好地搜索一些优化的模式。

但在任何情况下,编译器都不会试图寻找表示复杂算法的模式。当然,你可以尝试这样做,但这是无用的,而且雇佣一个优秀的程序员比尝试实现一个试图解决停机问题的系统更实惠。

最新更新