假设情况:
我要对 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,如需转载请自行联系原作者