且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

代码优化之结构体成员引用

更新时间:2022-09-13 20:56:00

假设情况:
我要对 a->b->c->d 中的这个d进行处理, 下面代码中有10行语句都用到前面这串东西, 也就是说在10次调用时,每次都需要去跳转4次内存地址来得到D的数值,  那么一共跳转了40次;
如果我在前面用 p = a->b->c->d; 后面的10次调用都直接用这个p, 是否能否达到减少内存跳转次数,以消费4 BYTE的内存空间来减少运行时内存跳转次数呢.

问题的关键是 a->b->c->d 这种东西是运行到那句汇编时直接从寄存器里一下拿到d的地址跳过去,还是得依次跳四次? 如果是前者的话,我们花个冗余变量就不值得,如果是后者的话,在多次调用的场合就值得去花这个临时变量了。


具体验证:

总体思路就是写试验代码,看编译出来的汇编。下文中的试验代码和编译结果是做论据用的,可以直接看各阶段的结论。

--------------------------试验代码壹--------------------------
struct haha
{
 int* d;
};

struct hehe
{
 haha* c;
};

struct hoho
{
 hehe* b;
};

struct heihei
{
 hoho* a;
};

void WinMain()
{
 int walzer = 0;
 heihei wahaha;
 int* p = wahaha.a->b->c->d;

 wahaha.a->b->c->d = &walzer;
 
 *(wahaha.a->b->c->d) = 100;

 *p = 255;

 *(wahaha.a->b->c->d) = 150;   //test one more time

 *p = 200;

}

--------------------------编译结果壹--------------------------
在EVC 4.00.16.10.0 中使用EVC自带编译器的结果
PS. 关于如何得到下面这个List File.  首先在Optimizations里必须选择Disable(Debug), 否则生成的汇编和C语句就没有对应关系了. 然后在EVC的Project Settings点到要生成的源文件,在右边窗口勾起 Generate browse info, 然后在Listing file type的下拉菜单里选择自己喜欢的风格(反正不能是no listing),  最后还必须指定List File Name, 光有默认时那个PATH是不行的. 参考下图:

代码优化之结构体成员引用

下面这个就是PtrTestWahaha.txt的全部内容
; Listing generated by Microsoft (R) Optimizing Compiler Version 12.20.9775

 TTL D:\SOURCE_CODE\Bragi\PtrTest\PtrTest.cpp
 CODE32

  00000    AREA  |.drectve|, DRECTVE
 DCB "-defaultlib:coredll.lib "
 DCB "-defaultlib:corelibc.lib "

 EXPORT |WinMain|

  00000    AREA  |.pdata|, PDATA
|$T286| DCD |WinMain|
 DCD 0x40002101
; Function compile flags: /Odt
; File D:\SOURCE_CODE\Bragi\PtrTest\PtrTest.cpp

  00000    AREA  |.text|, CODE, ARM

  00000   |WinMain| PROC

; 22   : {

  00000 e24dd00c  sub       sp, sp, #0xC
  00004   |$M284|

; 23   :  int walzer = 0;

  00004 e3a00000  mov       r0, #0
  00008 e58d0008  str       r0, [sp, #8]

; 24   :  heihei wahaha;
; 25   :  int* p = wahaha.a->b->c->d;

  0000c e59d1004  ldr       r1, [sp, #4]
  00010 e5910000  ldr       r0, [r1]
  00014 e5902000  ldr       r2, [r0]
  00018 e5921000  ldr       r1, [r2]
  0001c e58d1000  str       r1, [sp]

; 26   : 
; 27   :  wahaha.a->b->c->d = &walzer;

  00020 e59d0004  ldr       r0, [sp, #4]
  00024 e5901000  ldr       r1, [r0]
  00028 e5912000  ldr       r2, [r1]
  0002c e28d0008  add       r0, sp, #8
  00030 e5820000  str       r0, [r2]

; 28   :  
; 29   :  *(wahaha.a->b->c->d) = 100;

  00034 e59d1004  ldr       r1, [sp, #4]
  00038 e5910000  ldr       r0, [r1]
  0003c e5902000  ldr       r2, [r0]
  00040 e5921000  ldr       r1, [r2]
  00044 e3a00064  mov       r0, #0x64
  00048 e5810000  str       r0, [r1]

; 30   : 
; 31   :  *p = 255;

  0004c e59d2000  ldr       r2, [sp]
  00050 e3a000ff  mov       r0, #0xFF
  00054 e5820000  str       r0, [r2]

; 32   : 
; 33   :  *(wahaha.a->b->c->d) = 150;

  00058 e59d1004  ldr       r1, [sp, #4]
  0005c e5910000  ldr       r0, [r1]
  00060 e5902000  ldr       r2, [r0]
  00064 e5921000  ldr       r1, [r2]
  00068 e3a00096  mov       r0, #0x96
  0006c e5810000  str       r0, [r1]

; 34   : 
; 35   :  *p = 200;

  00070 e59d2000  ldr       r2, [sp]
  00074 e3a000c8  mov       r0, #0xC8
  00078 e5820000  str       r0, [r2]

; 36   : 
; 37   : }

  0007c e28dd00c  add       sp, sp, #0xC
  00080 e12fff1e  bx        lr
  00084   |$M285|

    ENDP  ; |WinMain|

 END
--------------------------结论壹--------------------------

在代码中对写wahaha.a->b->c->d地址和写p地址各尝试了两次,从编译结果来看,前者需要先分四句汇编跳转四次后,才能得到那个被套在第四层结构体里的目标。而后者只需要一步跳转。很显然所以在”多次“操作被嵌套多层的结构体成员时,先用一个指针引出,然后对这个临时指针操作,是的确可以提高代码运行效率的,嵌套越多层用这种方法效率就越高。在这里我注意到了跳转似乎是不分指针成员(.)还是非指针成员( ->) 的。为了确认这点我很好奇地把上面的结构体嵌套全部改成非指针成员再看看。


--------------------------试验代码贰--------------------------
再粘那么多代码文章就太冗余了,选取主要部分吧

int* p = &(wahaha.a.b.c.d);

wahaha.a.b.c.d = 100;
*p = 255;
wahaha.a.b.c.d = 150;
*p = 200;
--------------------------编译结果贰--------------------------
; 23   :  int walzer = 0;

  00004 e3a00000  mov       r0, #0
  00008 e58d0008  str       r0, [sp, #8]

; 24   :  heihei wahaha;
; 25   :  int* p = &(wahaha.a.b.c.d);

  0000c e28d1004  add       r1, sp, #4
  00010 e58d1000  str       r1, [sp]

; 26   : 
; 27   :  wahaha.a.b.c.d = 100;

  00014 e3a00064  mov       r0, #0x64
  00018 e58d0004  str       r0, [sp, #4]

; 28   : 
; 29   :  *p = 255;

  0001c e59d1000  ldr       r1, [sp]
  00020 e3a000ff  mov       r0, #0xFF
  00024 e5810000  str       r0, [r1]

; 30   : 
; 31   :  wahaha.a.b.c.d = 150;

  00028 e3a02096  mov       r2, #0x96
  0002c e58d2004  str       r2, [sp, #4]

; 32   : 
; 33   :  *p = 200;

  00030 e59d0000  ldr       r0, [sp]
  00034 e3a010c8   mov       r1, #0xC8
  00038 e5801000  str       r1, [r0]

--------------------------结论贰--------------------------

OHNO,伤心死了,和我的猜想正好相反。对结构体中非指针成员的变量进行操作,直接赋值反而比先拿指针再操作指针还要快些。偶不甘心,注意看下会发现这里有点赖皮,是因为wahaha.a.b.c.d的地址在前面可以很简单地通过堆栈指针SP和它后面的立即数来获得。我试图在前面写了一堆冗余代码捣乱一下,把SP踢远一点让他没那么容易找回来。结果很沮丧地发现SP仍然很容易地右移一个立即数的位数就可以找回wahaha.a.b.c.d。 所以对非指针成员的多层嵌套中,直接使用这个成员变量,会比指针引出后操作指针要来得快些。把两次的编译结果相比较,就会发现堆栈指针能一步找到的地址,似乎是在结构体多层嵌套中,第一个指针跳转前面的那堆东西。在代码壹里面,通过SP一步找到的是wahaha.a,然后跳转3次得到d;而在代码二中,通过SP一步找到的就是wahaha.a.b.c.d

所以,我们需要一个超级无敌变态的N重嵌套结构体调用,来推出我们优化率的公式。由于我在汇编方面比较小白,只能用比较弱的方法来尝试推导。下面代码里用了10重嵌套,指针和非指针成员混用,并且加入一些reserved成员以使各步中的立即数有所区别。我们现在的目标已经不是使用临时的指针变量好或不好了,而是推出一个公式,用公式来说话


--------------------------编译结果叁--------------------------
; 67   :  p = &(wahaha);

  00004 e28d0008  add       r0, sp, #8
  00008 e58d0000  str       r0, [sp]

; 68   :  p = &(wahaha.h1);

  0000c e28d100c  add       r1, sp, #0xC
  00010 e58d1000  str       r1, [sp]

; 69   :  p = &(wahaha.h1.h2);

  00014 e28d0010  add       r0, sp, #0x10
  00018 e58d0000  str       r0, [sp]

; 70   :  p = wahaha.h1.h2.ph3;

  0001c e59d1014  ldr       r1, [sp, #0x14]
  00020 e58d1000  str       r1, [sp]

; 71   :  p = &(wahaha.h1.h2.ph3->h4);

  00024 e59d0014  ldr       r0, [sp, #0x14]
  00028 e2801004  add       r1, r0, #4
  0002c e58d1000  str       r1, [sp]

; 72   :  p = &(wahaha.h1.h2.ph3->h4.h5);

  00030 e59d0014  ldr       r0, [sp, #0x14]
  00034 e2801008  add       r1, r0, #8
  00038 e58d1000  str       r1, [sp]

; 73   :  p = wahaha.h1.h2.ph3->h4.h5.ph6;

  0003c e59d0014  ldr       r0, [sp, #0x14]
  00040 e590100c  ldr       r1, [r0, #0xC]
  00044 e58d1000  str       r1, [sp]

; 74   :  p = &(wahaha.h1.h2.ph3->h4.h5.ph6->h7);

  00048 e59d0014  ldr       r0, [sp, #0x14]
  0004c e590100c  ldr       r1, [r0, #0xC]
  00050 e2812004  add       r2, r1, #4
  00054 e58d2000  str       r2, [sp]

; 75   :  p = wahaha.h1.h2.ph3->h4.h5.ph6->h7.ph8;

  00058 e59d0014  ldr       r0, [sp, #0x14]
  0005c e590100c  ldr       r1, [r0, #0xC]
  00060 e5912008  ldr       r2, [r1, #8]
  00064 e58d2000  str       r2, [sp]

; 76   :  p = wahaha.h1.h2.ph3->h4.h5.ph6->h7.ph8->ph9;

  00068 e59d0014  ldr       r0, [sp, #0x14]
  0006c e590100c  ldr       r1, [r0, #0xC]
  00070 e5912008  ldr       r2, [r1, #8]
  00074 e5920004  ldr       r0, [r2, #4]
  00078 e58d0000  str       r0, [sp]

; 77   :  p = &(wahaha.h1.h2.ph3->h4.h5.ph6->h7.ph8->ph9->Num);

  0007c e59d1014  ldr       r1, [sp, #0x14]
  00080 e591000c  ldr       r0, [r1, #0xC]
  00084 e5902008  ldr       r2, [r0, #8]
  00088 e5921004  ldr       r1, [r2, #4]
  0008c e2810004  add       r0, r1, #4
  00090 e58d0000  str       r0, [sp]

; 78   : 
; 79   : 
; 80   :  wahaha.h1.h2.ph3->h4.h5.ph6->h7.ph8->ph9->Num = 100;

  00094 e59d1014  ldr       r1, [sp, #0x14]
  00098 e591000c  ldr       r0, [r1, #0xC]
  0009c e5902008  ldr       r2, [r0, #8]
  000a0 e5921004  ldr       r1, [r2, #4]
  000a4 e3a00064  mov       r0, #0x64
  000a8 e5810004  str       r0, [r1, #4]

; 81   : 
; 82   :  *walzer = 255;

  000ac e59d2018  ldr       r2, [sp, #0x18]
  000b0 e3a000ff  mov       r0, #0xFF
  000b4 e5820000  str       r0, [r2]

; 83   : 
; 84   :  wahaha.h1.h2.ph3->h4.h5.ph6->h7.ph8->ph9->Num = 150;

  000b8 e59d1014  ldr       r1, [sp, #0x14]
  000bc e591000c  ldr       r0, [r1, #0xC]
  000c0 e5902008  ldr       r2, [r0, #8]
  000c4 e5921004  ldr       r1, [r2, #4]
  000c8 e3a00096  mov       r0, #0x96
  000cc e5810004  str       r0, [r1, #4]

; 85   : 
; 86   :  *walzer = 200;

  000d0 e59d2018  ldr       r2, [sp, #0x18]
  000d4 e3a000c8  mov       r0, #0xC8
  000d8 e5820000  str       r0, [r2]

--------------------------分析--------------------------
结果已经很明显了,一个赋值语句,不论是取结构体中的成员,还是对其赋值,至少需要两句汇编,一句是通过SP取值,第二句给目标赋值。而其中如果遇到指针成员的跳转,则每次跳转需要一句。我们假设在某次调用中有N次跳转,也就是N个”->“,那么编译出来就是 2+N 句汇编, 其中N+1句是为了取到源的。如果使用一个临时指针变量来操作,那么只有在对临时指针赋值那句是2+N,赋值的最后一句汇编一定是把这个临时指针压入堆栈,后面每次调用中的取源都只需要一句汇编通过SP从堆栈中取出就行了。假设在后面对一这个成员变量进行了M次操作。那么为了取数就必须有这么多句汇编的开销:
不使用临时指针变量:  M*(N+1)
使用临时指针变量:      (N+2)+M*1
这就是我们需要的公式了!

--------------------------总结和公式--------------------------
忽略4 BYTE的内存开销,单就运行效率来说,
假设调用某个嵌套的成员变量用了N个"->",而这个变量被调用M次的场景下,当 N+2+M < M*(N+1)时,使用临时的指针变量就的确能达到提高效率的目的,公式不满足时就不应该用临时的指针变量。

而另一方面,内存开销属于空间,运行效率属于时间,分在不同的维度内,所以这两者是无法比较并建立公式的,


本文转自Walzer博客园博客,原文链接:http://www.cnblogs.com/walzer/archive/2006/08/23/483973.html,如需转载请自行联系原作者