C++/C-汇编级别的问题

7
  1. 在函数内使用全局变量(C/C++),变量的值是直接从寄存器还是栈中取出来的?

  2. 为什么有边界的循环(for循环)比无边界的循环(while/do while)更容易进行优化?

  3. 为什么返回值不如通过引用传递值好?

如果可能,请给出汇编级别的描述。


9
(1) neither -> 两者都不(表示否定选择中的两个选项) (2) they aren't -> 它们不是(指代单数或复数物体,表示否定) (3) it needn't be, because of NRVO. -> 因为有 NRVO,它不需要是这样的。 - Steve Jessop
你在第三点中的意思是什么?是通过引用传递参数给函数,还是将函数值返回给其中一个函数参数。 - Pawel Zubrycki
@Pawel:RVO 意味着返回值优化,这意味着在原地构造值(在其返回的位置),而不是产生复制的成本。当此优化启动时,按值返回的成本不会比传递引用或指针更高。 - Matthieu M.
我的问题是问 Pappu,不是 Steve。 - Pawel Zubrycki
4个回答

4

1) 全局变量只是进程虚拟地址空间中的一个地址,通常会从应用程序负载分配的地址中获取。如果该全局变量最近被使用过,则编译器可能会将其缓存到寄存器中。

2) 他们不这样做。

3) 返回值通常需要复制数据。如果数据是简单类型(例如int或float),那么可以通过寄存器返回。如果对象太大而无法适应寄存器,则编译器必须在堆栈上分配对象的空间,然后将返回的数据复制到此分配的空间中。将值作为引用传递通常是通过传递指向数据的指针来实现的。因此,您通过直接修改该内存地址处的数据来返回该值。不发生复制,因此速度更快。请注意,返回值优化(RVO)可能意味着将返回值作为引用传递没有优势。同样,正如评论中指出的那样,C++0x的新移动构造函数也可以提供与RVO相同的优势。

在我看来,不需要使用汇编示例来解释这些内容。


1
全局变量通常不会从堆中分配(除非您的意思与我对堆的理解不同)。 - CB Bailey
2
  1. 返回一个值需要数据的复制 - 但是不使用移动构造函数。
- Zach Saw
1
关于第二点,我认为for循环比while循环有更多的优化空间。有人能解释一下为什么吗? - Rajeev
3
从操作系统的角度来看,程序的静态数据可能是从堆中分配的。从 C++ 标准的角度来看,“堆”是由 std::make_heap 等操纵的一种数据结构,与内存分配无关。从程序员的角度来看,呃,你可以有自己的看法。不过要小心,如果你将代码定义为“在堆上”,那么在哈佛架构上,当发现“堆”根据这个定义占用了两个不兼容的内存库时,你可能会有些惊讶。 - Steve Jessop
1
@Karl @Goz 标准称其为“动态存储”。 - Yakov Galka
显示剩余14条评论

1

1)全局变量由链接器静态分配(虽然它可以是模块基址的偏移量,但不一定是固定地址)。尽管如此,函数通常会从直接地址读取全局变量,从偏移量+堆栈指针读取局部变量,从偏移量+对象基指针读取类字段。全局变量的值可以缓存在寄存器中以供后续读取,除非它被声明为“易失性”。

2)这并不是关于for/do/while选择的问题,而是计算迭代次数有多容易,以便编译器能够决定是否展开、向量化和/或并行化循环。例如,在这里编译器将知道迭代次数:

for( i=0; i<8; i++ ) { j = 1 << i; XXX }

而在这里它不会:

for( j=1; j<256; j<<=1 ) { XXX }

for循环可能具有更易于编译器理解的结构。

3)如果它是基本类型的值(char/short/int等),通过引用返回它会慢一些(尽管有时编译器可以优化它)。 但对于较大的结构,引用/指针可以减少编译器的工作量,如果编译器无法避免创建一些临时副本等,则确实可能更快。

更新: 好的,这里有一个更具体的例子:

#include <stdio.h>

int main( void ) {

  int a,b, i,j,s1,s2;

  a = 123 + printf(""); // unknown in compile time
  s1 = 1; 
  // bit reverse loop v1, gets unrolled
  for( i=0; i<8; i++ ) { j = 1 << i; s1 += s1 + ((a&j)>0); }
  s1 -= 256;

  b = s1 + printf("");
  // bit reverse loop v2, not unrolled
  for( s2=1; s2<256; s2+=s2+(j>0) ) { j = b & s2; b -= j; }
  s2 -= 256;

  printf( "a=%02X s1=%02X s2=%02X\n", a, s1, s2 );
}

这里提供gcc/intelc的汇编代码清单:http://nishi.dreamhosters.com/u/1.zip


假设XXX没有以复杂的方式修改j,那么优化编译器完全有可能确定第二个循环的迭代次数。 - Ben Voigt
一个更好的循环示例是遍历链表,由于迭代次数未知(因此无法展开),编译器不可能知道迭代次数:for ( ptr_t p = first; p; p = p->next ) {。在第二种情况下,正如Ben所指出的那样,优化器可以知道该循环将迭代7次(除非XXX修改了j)。+1是因为它不是for/while,而是编译器对循环的了解程度。(另一个更简单的例子:for ( int i = 0; i < j; ++i ) {其中j不是常量。) - David Rodríguez - dribeas

1

一般情况下(准确地说很难),全局变量是从内存中检索而不是从堆栈中检索(除非已经缓存在寄存器中),循环可以根据编译器对循环执行的信息进行优化(它是否可以执行循环展开?)在第三种情况下,这取决于实际代码。由于前两个问题已经在其他问题中处理过了,我将专注于第三个问题。

有一种常见的优化称为(命名)返回值优化(N)RVO,编译器可以执行该优化以避免不必要的复制。

// RVO                  // NRVO             // cannot perform RVO
type foo() {            type bar() {        type baz() {
   value a;                type a;             type a,b; 
   // operate on a         // modify a         // pass a and b to other functions
   return type(a);         return a;           if ( random() > x ) return a;
}                       }                      else return b;
                                            }

foobar中,编译器能够分析代码并确定foo中的临时type(a)bar中的命名本地变量a是函数的返回值,因此它可以构造这些对象以代替返回值(根据调用约定),避免复制。与之相比,baz必须在实际知道哪个返回之前创建对象ab。在这种情况下,编译器无法优化任何内容,必须执行操作,并且只有在最后将ab之一复制到返回值。
每当编译器执行(N)RVO或者实际上不可能执行时,将函数签名更改为通过引用接收对象将不会提供性能优势,并且会使创建新对象的函数在调用处的代码不易读。

这应该作为一个经验法则,但需要注意的是,总会有例外情况和某些情况下其中一种方法可能略微更具性能优势。但对于大多数情况,除非测量性能证明了不同的写法更好,否则应尽可能接近设计语义编写代码。如果函数创建新对象,则按值返回它;如果函数修改对象,则通过引用传递。

有些特殊情况可以是创建向量并在紧密循环中调用的函数,其中仅传递一个由引用传递的单个向量,在函数中进行清除然后填充将减少内存分配的数量(向量中的clear()不会释放内存,因此不需要在下一次迭代中重新分配它)。

另一方面,当函数调用被链接在一起,并且使用合适的按返回值或按值传递的组合时,您可能通过不传递引用来避免额外的副本——非const引用需要非临时对象。


1

首先,您没有指定目标平台,arm、x86、6502、zpu等。

1)当在函数内部使用全局变量(C/C++)时,它是直接从寄存器还是从堆栈中获取?

由于您没有明确说明,因此全局变量可以按值传递、按引用传递或不传递并直接在函数中使用。

按值传递取决于代码/编译器/目标,这些都是您没有指定的。因此,全局变量的值或地址可以根据编译器/目标的调用约定放入寄存器或堆栈中。通过寄存器传递的项目有时会在堆栈上有一个占位符,以防函数需要的寄存器比可用的寄存器多。因此,按值传递时,全局包含的值最初在寄存器或堆栈中访问。

按引用传递与按值传递基本相同,只是传递的是全局变量的地址,而不是值,该地址可以通过寄存器或堆栈传递,具体取决于编译器/目标。这种方式的区别在于,您可以直接从/到其内存位置访问全局变量,但这是按引用传递的特性。

如果全局变量在函数中直接使用,那么它是否从其固定的内存位置直接访问还是寄存器加载该内存位置并且该值从寄存器操作取决于代码/编译器/目标。在这种情况下不使用堆栈,因此答案是(非堆栈)内存或寄存器。

2)为什么有界循环(for循环)被认为比无界循环(while循环/do while)具有更多的优化空间?

这取决于代码、编译器和目标,我想不出一个通用的情况,其中一个比另一个更好。

3)为什么返回值不如通过引用传递值好?

如果有任何微小的性能提升,那也是非常微妙的。这严重依赖于代码、编译器和目标。有些情况下,通过引用略微快一些,有些情况下,通过值略微快一些。比较这两者,差异在于地址或数据必须复制到/从寄存器或堆栈上的次数。最多可以节省几个mov或load/store指令。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接