Code size matter


几年前写过一篇关于ARM的汇编的文章,那时候的iPhone还是32bit的,一转眼10年过去,现在的ARM基本早已经是64bit了,所对应的汇编指令也发生了一些变化。这篇文章并不会重复之前关于汇编的基本内容,而是会通过一些汇编知识来观察不同代码对code size的影响。

Register Basics

  1. 所有ARM64的指令都是基于对寄存器的操作,其中每个寄存器大小为8 bytes,在ARM64的设备上,我们可以操作大约32个寄存器。
  2. 显然,数据是无法都存在寄存器的,为了从内存中读写数据,需要用到所谓的load和store指令,其中ldr指令将内存中的数据读入寄存器,str将寄存器中的数据写入内存,详细参考这里

A Simple Exercise

下面这两个函数很简单,做的事情也相同,但第二个函数会产生较多的代码

#import <vector>

int useIndex(const std::vector<int>& v) {
    return v[1];
}

int useAt(const std::vector<int>& v) {
    return v.at(1);
}

xcrun --sdk iphoneos clang -arch arm64 -c -Oz进行编译,并用symbols来查看这两个函数的大小

test.o [arm64, 0.006064 seconds]:
    null-uuid                            /Users/taox/Projects/CodeBase/cpp/perf/test.o [OBJECT, FaultedFromDisk]  
        0x0000000000000000 (    0xb8)  SEGMENT
            0x0000000000000000 (    0x50) __TEXT __text
                0x0000000000000000 (     0xc) useIndex(std::__1::vector<int, std::__1::allocator<int> > const&) [FUNC, EXT, NameNList, ...
                0x000000000000000c (    0x1c) useAt(std::__1::vector<int, std::__1::allocator<int> > const&) [FUNC, EXT, NameNList, Man...
                0x0000000000000028 (    0x28) std::__1::vector<int, std::__1::allocator<int> >::at(unsigned long) const [FUNC, EXT, Nam...
            0x0000000000000050 (     0x8) __DATA __objc_imageinfo
            0x0000000000000058 (    0x60) __LD __compact_unwind

可以看到

  • useIndex的大小为0xc(12字节)
  • useAt的大小为0x1c(28字节)
  • vector::at的大小为0x28 (40字节)

相加正好是80字节(0x50),和__TEXT段大小一致,再来对比一下汇编代码


useIndex:
; %bb.0:
	ldr	x8, [x0]
	ldr	w0, [x8, #4]
	ret


useAt:
; %bb.0:
	mov	w1, #1
	bl	std::vector::at
	ldr	w0, [x0]
	ret

useIndex的汇编代码比较简洁,这里编译器应该是inline了某些代码,因为v[i]是一个C++函数,这里应该直接inline了。注意w0相当于x0的lower 4 bytes,#4是offse表示skip 4 bytes。ret返回的结果通常保存在寄存器x[0]

  • ldr x8, [x0] 相当于*x0 -> x8或者 v.begin -> x8
  • ldr w0, [x8, #4] 相当于*(x8+4) -> w0

我们接着分析useAt的代码。v.at(1)会被编译为at(v, 1),因此w1中保存index=1。接着blvector::at

std::__1::vector<int, std::__1::allocator<int> >::at(unsigned long) const
; %bb.0:
	ldp	x8, x9, [x0]
	sub	x9, x9, x8
	cmp	x1, x9, asr #2
	b.hs	LBB2_2
; %bb.1:
	add	x0, x8, x1, lsl #2
	ret
LBB2_2:
	bl	std::__1::__vector_base_common<true>::__throw_out_of_range() const
	.cfi_endproc

vector::at的代码比较多,我们逐条分析

  • ldp x8, x9, [x0] 这里用到ldp,表示load pair,它会一次load两个连续的值到寄存器中。[x0]中保存的是v的地址,因此x8v.begin()x9v.end()
  • sub x9, x9, x8 是计算size,单位是bytes,相当于x9 <- v.size()*sizeof(int)
  • cmp x1, x9, asr #2,这里x1保存参数index,x9 asr #2是右移操作,相当于除法,除数为2^2 = 4,也就是说x9 <- v.size()*sizeof(int) / sizeof(int)。接下来的cmp操作用来检测index是否越界
  • b.hs是conditional jump,如果越界,则jump到LBB2_2,进而throw exception
  • add x0, x8, x1, lsl #2 上面提到x8保存的是v.beginlsl是左移,因此这里计算的是 x0 <- x8 + x1<<2,左移相当于乘法,继续展开相当于 x0 <- v.begin() + index*sizeof(int)实际上就是this->beign[index]

由此,我们可以推测vector的实现大概为

template<typename T>
const T& std::vector<T>::at(unsigned long index) const {
    if(index >= size()) {
        std::__1::__vector_base_common<true>::__throw_out_of_range();
    }
    return this->beign[index];
}

Resources