Pet*_*des 21
类的存储方式与结构完全相同,除非它们具有虚拟成员.在这种情况下,有一个隐含的vtable指针作为第一个成员(见下文).
结构存储为连续的内存块(如果编译器不优化它或将成员值保存在寄存器中).在结构对象中,其元素的地址按成员的定义顺序增加.(来源:http://en.cppreference.com/w/c/language/struct).我链接了C定义,因为在C++中struct意味着class(public:作为默认值而不是private:).
可以想象一个struct或者class作为一个字节块,它可能太大而不适合寄存器,但是它被复制为"值". 汇编语言没有类型系统; 内存中的字节只是字节,它不需要任何特殊指令来存储double来自浮点寄存器的a并将其重新加载到整数寄存器中.或者做一个未对齐的加载并得到1的最后3个字节int和下一个的第一个字节.A struct只是在内存块之上构建C类型系统的一部分,因为内存块很有用.
这些字节块可以具有静态(全局或static),动态(malloc或new)或自动存储(本地变量:临时在堆栈上或寄存器中,在普通CPU上的普通C/C++实现中).块中的布局是相同的(除非编译器优化了struct局部变量的实际内存;请参阅下面的内联返回结构的函数的示例.)
结构或类与任何其他对象相同.在C和C++术语中,甚至int是一个对象:http://en.cppreference.com/w/c/language/object.即一个连续的字节块,你可以记忆(除了C++中的非POD类型).
您正在编译的系统的ABI规则指定插入填充的时间和位置,以确保每个成员具有足够的对齐,即使您执行类似操作struct { char a; int b; }; (例如,x86-64 System V ABI,用于Linux和其他非-Windows系统指定int是32位类型,在内存中获得4字节对齐 .ABI指的是C和C++标准留下"依赖于实现"的东西,因此该ABI的所有编译器都可以生成代码这可以调用彼此的功能.)
请注意,您可以使用它offsetof(struct_name, member)来查找结构布局(在C11和C++ 11中).另请参见alignofC++ 11或_AlignofC11.
程序员可以很好地命令struct成员避免在填充上浪费空间,因为C规则不允许编译器为你排序结构.(例如,如果你有一些char成员,将它们放在至少4个组中,而不是与更宽的成员交替.从大到小的排序是一个简单的规则,记住指针在公共平台上可能是64位或32位.)
有关ABI等的更多详细信息,请访问https://stackoverflow.com/tags/x86/info.Agner Fog的优秀网站包括ABI指南以及优化指南.
class foo {
int m_a;
int m_b;
void inc_a(void){ m_a++; }
int inc_b(void);
};
int foo::inc_b(void) { return m_b++; }
Run Code Online (Sandbox Code Playgroud)
编译(使用http://gcc.godbolt.org/):
foo::inc_b(): # args: this in RDI
mov eax, DWORD PTR [rdi+4] # eax = this->m_b
lea edx, [rax+1] # edx = eax+1
mov DWORD PTR [rdi+4], edx # this->m_b = edx
ret
Run Code Online (Sandbox Code Playgroud)
如您所见,this指针作为隐式的第一个参数传递(在rdi中,在SysV AMD64 ABI中). m_b存储在struct/class开头的4个字节处.注意巧妙地使用lea实现后增量运算符,保留旧值eax.
没有代码inc_a被发出,因为它是在类声明中定义的.它被视为inline非成员函数.如果它真的很大并且编译器决定不内联它,它可以发出它的独立版本.
C++对象与C结构真正不同的地方是涉及虚拟成员函数时.对象的每个副本都必须携带一个额外的指针(对于其实际类型的vtable).
class foo {
public:
int m_a;
int m_b;
void inc_a(void){ m_a++; }
void inc_b(void);
virtual void inc_v(void);
};
void foo::inc_b(void) { m_b++; }
class bar: public foo {
public:
virtual void inc_v(void); // overrides foo::inc_v even for users that access it through a pointer to class foo
};
void foo::inc_v(void) { m_b++; }
void bar::inc_v(void) { m_a++; }
Run Code Online (Sandbox Code Playgroud)
; This time I made the functions return void, so the asm is simpler
; The in-memory layout of the class is now:
; vtable ptr (8B)
; m_a (4B)
; m_b (4B)
foo::inc_v():
add DWORD PTR [rdi+12], 1 # this_2(D)->m_b,
ret
bar::inc_v():
add DWORD PTR [rdi+8], 1 # this_2(D)->D.2657.m_a,
ret
# if you uncheck the hide-directives box, you'll see
.globl foo::inc_b()
.set foo::inc_b(),foo::inc_v()
# since inc_b has the same definition as foo's inc_v, so gcc saves space by making one an alias for the other.
# you can also see the directives that define the data that goes in the vtables
Run Code Online (Sandbox Code Playgroud)
有趣的事实:add m32, imm8比inc m32大多数英特尔CPU 更快(负载的微融合+ ALU uops); 旧的Pentium4建议避免的罕见情况之一inc仍然适用.gcc总是避免inc,即使它会节省代码大小没有缺点:/ INC指令与ADD 1:这有关系吗?
void caller(foo *p){
p->inc_v();
}
mov rax, QWORD PTR [rdi] # p_2(D)->_vptr.foo, p_2(D)->_vptr.foo
jmp [QWORD PTR [rax]] # *_3
Run Code Online (Sandbox Code Playgroud)
(这是一个优化的尾调:jmp替换call/ ret).
将movvtable地址从对象加载到寄存器中.这jmp是一个内存间接跳转,即从内存加载一个新的RIP值. 跳转目标地址vtable[0],即vtable中的第一个函数指针. 如果有另一个虚函数,则mov不会改变,但jmp会使用jmp [rax + 8].
vtable中的条目顺序可能与类中声明的顺序相匹配,因此在一个转换单元中重新排序类声明将导致虚函数转到错误的目标.就像重新排序数据成员一样,会改变类的ABI.
如果编译器有更多信息,它可以使调用虚拟化.例如,如果它可以证明foo *总是指向一个bar物体,它可以内联bar::inc_v().
GCC甚至会在编译时弄清楚类型可能是什么时进行推测性虚拟化.在上面的代码中,编译器看不到任何继承的类,所以指向一个对象而不是一些派生类是一个很好的选择.barbar*bar
void caller_bar(bar *p){
p->inc_v();
}
# gcc5.5 -O3
caller_bar(bar*):
mov rax, QWORD PTR [rdi] # load vtable pointer
mov rax, QWORD PTR [rax] # load target function address
cmp rax, OFFSET FLAT:bar::inc_v() # check it
jne .L6 #,
add DWORD PTR [rdi+8], 1 # inlined version of bar::inc_v()
ret
.L6:
jmp rax # otherwise tailcall the derived class's function
Run Code Online (Sandbox Code Playgroud)
请记住,a foo *实际上可以指向派生bar对象,但bar *不允许指向纯foo对象.
这只是一个赌注; 虚函数的一部分是可以扩展类型而无需重新编译在基类型上运行的所有代码.这就是为什么它必须比较函数指针并回退到间接调用(在这种情况下为jmp tailcall),如果它是错误的.编译器启发式方法决定何时尝试它.
请注意,它正在检查实际的函数指针,而不是比较vtable指针.bar::inc_v()只要派生类型没有覆盖该虚函数,它仍然可以使用内联.覆盖其他虚拟函数不会影响这个函数,但需要不同的vtable.
允许扩展而无需重新编译对于库来说很方便,但也意味着大程序各部分之间的松散耦合(即,您不必在每个文件中包含所有标头).
但是这会给某些用途带来一些效率成本:C++虚拟调度只能通过指向对象的指针来工作,因此你不能拥有没有黑客的多态数组,也不能通过指针数组进行昂贵的间接调整(这会破坏许多硬件和软件的优化) :在c ++中最快的实现简单,虚拟,观察者类型的模式?).
如果你想要某种类型的多态/分派,但只需要一组封闭的类型(即在编译时都知道),你可以使用union + enum+switch手动完成,或者使用std::variant<D1,D2>union和std::visitdispatch,或者其他各种类型方法.另请参阅c ++ 中多态类型的连续存储和最简单,虚拟,观察者类型的最快实现?.
使用a struct不会强制编译器实际将内容放入内存,只需要一个小数组或指向局部变量的指针.例如,返回structby值的内联函数仍然可以完全优化.
as-if规则适用:即使结构在逻辑上具有一些内存存储,编译器也可以使asm将所有需要的成员保存在寄存器中(并进行转换,这意味着寄存器中的值不对应于变量的任何值)或临时在C++抽象机中"运行"源代码).
struct pair {
int m_a;
int m_b;
};
pair addsub(int a, int b) {
return {a+b, a-b};
}
int foo(int a, int b) {
pair ab = addsub(a,b);
return ab.m_a * ab.m_b;
}
Run Code Online (Sandbox Code Playgroud)
# The non-inline definition which actually returns a struct
addsub(int, int):
lea edx, [rdi+rsi] # add result
mov eax, edi
sub eax, esi # sub result
# then pack both struct members into a 64-bit register, as required by the x86-64 SysV ABI
sal rax, 32
or rax, rdx
ret
# But when inlining, it optimizes away
foo(int, int):
lea eax, [rdi+rsi] # a+b
sub edi, esi # a-b
imul eax, edi # (a+b) * (a-b)
ret
Run Code Online (Sandbox Code Playgroud)
请注意,即使按值返回结构也不一定会将其放入内存中.x86-64 SysV ABI传递并返回打包在一起的小结构.不同的ABI为此做出了不同的选择.
(抱歉,由于代码示例,我无法将此作为“评论”发布到 Peter Cordes 的答案,因此我必须将其发布为“答案”。)
旧的 C++ 编译器生成 C 代码而不是汇编代码。以下类:
class foo {
int m_a;
void inc_a(void);
...
};
Run Code Online (Sandbox Code Playgroud)
... 将产生以下 C 代码:
struct _t_foo_functions {
void (*inc_a)(struct _class_foo *_this);
...
};
struct _class_foo {
struct _t_foo_functions *functions;
int m_a;
...
};
Run Code Online (Sandbox Code Playgroud)
“类”变成“结构”,“对象”变成结构类型的数据项。所有函数在 C 中都有一个附加元素(与 C++ 相比):“this”指针。“struct”的第一个元素是指向类所有函数列表的指针。
所以下面的C++代码:
m_x=1; // implicit this->m_x
thisMethod(); // implicit this->thisMethod()
myObject.m_a=5;
myObject.inc_a();
myObjectp->some_other_method(1,2,3);
Run Code Online (Sandbox Code Playgroud)
...将在 C 中看起来如下:
_this->m_x=1;
_this->functions->thisMethod(_this);
myObject.m_a=5;
myObject.functions->inc_a(&myObject);
myObjectp->some_other_method(myObjectp,1,2,3);
Run Code Online (Sandbox Code Playgroud)
使用那些旧的编译器,C 代码被翻译成汇编程序或机器代码。您只需要知道在汇编代码中如何处理结构以及如何处理对函数指针的调用...
尽管现代编译器不再将 C++ 代码转换为 C 代码,但生成的汇编代码看起来仍然与您首先执行 C++ 到 C 的步骤相同。
“new”和“delete”将导致对内存函数的函数调用(您可以改为调用“malloc”或“free”)、构造函数或析构函数的调用以及结构元素的初始化。
| 归档时间: |
|
| 查看次数: |
6558 次 |
| 最近记录: |