16.5 Multidimensional arrays
多维数组和线性数组在本质上是一样的。 因为计算机内存是线性的,它是一维数组。但是一维数组可以很容易用来表现多维的。 比如数组a[3][4]元素可以放置在一维数组的12个单元中:
[0][0]
[0][1]
[0][2]
[0][3]
[1][0]
[1][4]
[1][5]
[1][6]
[2][0]
[2][7]
[2][8]
[2][9]
该二维数组在内存中用一维数组索引表示为:
1 | 2 | 3 | |
---|---|---|---|
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
为了计算我们需要的元素地址,首先,第一个索引乘以4(矩阵宽度),然后加上第二个索引。这种被称为行优先,C/C++和Python常用这种方法。行优先的意思是:先写入第一行,接着是第二行,…,最后是最后一行。 另一种方法就是列优先,主要用在FORTRAN,MATLAB,R等。列优先的意思是:先写入第一列,然后是第二列,…,最后是最后一列。 多维数组与此类似。 我们看个例子:
Listing 16.4: simple example
#include <stdio.h>
int a[10][20][30];
void insert(int x, int y, int z, int value)
{
a[x][y][z]=value;
};
16.5.1 x86
MSVC 2010:
Listing 16.5: MSVC 2010
_DATA SEGMENT
COMM _a:DWORD:01770H
_DATA ENDS
PUBLIC _insert
_TEXT SEGMENT
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_z$ = 16 ; size = 4
_value$ = 20 ; size = 4
_insert PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _x$[ebp]
imul eax, 2400 ; eax=600*4*x
mov ecx, DWORD PTR _y$[ebp]
imul ecx, 120 ; ecx=30*4*y
lea edx, DWORD PTR _a[eax+ecx] ; edx=a + 600*4*x + 30*4*y
mov eax, DWORD PTR _z$[ebp]
mov ecx, DWORD PTR _value$[ebp]
mov DWORD PTR [edx+eax*4], ecx ; *(edx+z*4)=value
pop ebp
ret 0
_insert ENDP
_TEXT ENDS
多维数组计算索引公式:address=6004x+304y+4z。因为int类型为32-bits(4字节),所以要乘以4。
Listing 16.6: GCC 4.4.1
public insert
insert proc near
x = dword ptr 8
y = dword ptr 0Ch
z = dword ptr 10h
value = dword ptr 14h
push ebp
mov ebp, esp
push ebx
mov ebx, [ebp+x]
mov eax, [ebp+y]
mov ecx, [ebp+z]
lea edx, [eax+eax] ; edx=y*2
mov eax, edx ; eax=y*2
shl eax, 4 ; eax=(y*2)<<4 = y*2*16 = y*32
sub eax, edx ; eax=y*32 - y*2=y*30
imul edx, ebx, 600 ; edx=x*600
add eax, edx ; eax=eax+edx=y*30 + x*600
lea edx, [eax+ecx] ; edx=y*30 + x*600 + z
mov eax, [ebp+value]
mov dword ptr ds:a[edx*4], eax ; *(a+edx*4)=value
pop ebx
pop ebp
retn
insert endp
GCC使用的不同的计算方法。为计算第一个操作值30y,GCC没有使用乘法指令。GCC的做法是:(???? + ????) ≪ 4 − (???? + ????) = (2????) ≪ 4 − 2???? = 2 ・ 16 ・ ???? − 2???? = 32???? − 2???? = 30????。因此30y的计算仅使用加法和移位操作,这样速度更快。
16.5.2 ARM + Non-optimizing Xcode (LLVM) + thumb mode
Listing 16.7: Non-optimizing Xcode (LLVM) + thumb mode
_insert
value = -0x10
z = -0xC
y = -8
x = -4
; allocate place in local stack for 4 values of int type
SUB SP, SP, #0x10
MOV R9, 0xFC2 ; a
ADD R9, PC
LDR.W R9, [R9]
STR R0, [SP,#0x10+x]
STR R1, [SP,#0x10+y]
STR R2, [SP,#0x10+z]
STR R3, [SP,#0x10+value]
LDR R0, [SP,#0x10+value]
LDR R1, [SP,#0x10+z]
LDR R2, [SP,#0x10+y]
LDR R3, [SP,#0x10+x]
MOV R12, 2400
MUL.W R3, R3, R12
ADD R3, R9
MOV R9, 120
MUL.W R2, R2, R9
ADD R2, R3
LSLS R1, R1, #2 ; R1=R1<<2
ADD R1, R2
STR R0, [R1] ; R1 - address of array element
; deallocate place in local stack, allocated for 4 values of int type
ADD SP, SP, #0x10
BX LR
非优化的LLVM代码在栈中保存了所有变量,这是冗余的。元素地址的计算我们通过公式已经找到了。
16.5.3 ARM + Optimizing Xcode (LLVM) + thumb mode
Listing 16.8: Optimizing Xcode (LLVM) + thumb mode
_insert
MOVW R9, #0x10FC
MOV.W R12, #2400
MOVT.W R9, #0
RSB.W R1, R1, R1,LSL#4 ; R1 - y. R1=y<<4 - y = y*16 - y = y*15
ADD R9, PC ; R9 = pointer to a array
LDR.W R9, [R9]
MLA.W R0, R0, R12, R9 ; R0 - x, R12 - 2400, R9 - pointer to a. R0=x*2400 + ptr to a
ADD.W R0, R0, R1,LSL#3 ; R0 = R0+R1<<3 = R0+R1*8 = x*2400 + ptr to a + y*15*8 =
; ptr to a + y*30*4 + x*600*4
STR.W R3, [R0,R2,LSL#2] ; R2 - z, R3 - value. address=R0+z*4 =
; ptr to a + y*30*4 + x*600*4 + z*4
BX LR
这里的小技巧没有使用乘法,使用移位、加减法等。 这里有个新指令RSB(逆向减法),该指令的意义是让第一个操作数像SUB第二个操作数一样可以应用LSL#4附加操作。 “LDR.W R9, [R9]”类似于x86下的LEA指令(B.6.2),这里什么都没有做,是冗余的。显然,编译器没有优化它。 # 位域
很多函数参数的输入标志使用了位域。当然,可以使用bool类型来替代,只是有点浪费。