Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Sandor Dargo
Sandor Dargo

Posted on • Originally published atsandordargo.com

Virtual functions and binary sizes

In the previous article of this series on binary sizes, we discussed how special functions' implementations - or their lack of - influence the size of the generated binary.

Our conclusion was that if we could, we should follow the rule of 0. Not only because of simplicity for the reader but also for the compiler. If that's not possible, we should not declare destructors virtual in vain. And if it's possible, we should=default non-virtual destructors in the header both for readability and for binary sizes.

On the other hand, if we must have a non-virtual destructor, we should clearly think about implementing (preferably with=default) the destructor in the.cpp file, in other words, out-of-line. While you might find it strange in terms of readability, it produced a smaller binary.

It's still worth noting that in most circumstances, a class with avirtual destructor contributes to a larger binary file.

But does it matter how many functions are virtual? Given the same amount of methods in a class, is there a difference in terms of binary size whether one or all of them are virtual?

That's the question we are going to discuss in this post.

What does thevirtual keyword do?

When any of a class-member function is declared with thevirtual keyword, it means that the compiler cannot know during compile-time which implementation of thevirtual function will be called.

How could it resolve during runtime which function is to be called?

As it's explained onJohnny's Software Lab, the way to do it is not standardized. Yet, most compilers do it in a similar way.

For each class that has at least onevirtual method, the compiler creates a virtual table. It's usually just referred to as avtable. Thevtable of each class contains a pointer to each of theirvirtual functions. A virtual table might not contain pointers only to the same class. If a derived class doesn'toverride a method of a base class, then it will point to that base class implementation.

When an object is created at runtime, there is also a virtual pointer (vpointer) created pointing to the right virtual table.

So there is onevpointer for each object created at run-time, but only onevtable per type which is created per compile-time.

I think we can rightly expect that therefore avirtual method - due to thevtable - results in an increased binary size and we could also expect that any additionalvirtual method will add to the size of the binary as thevtable grows, but only a little bit.

Validate our hypothesis

Now let's create twomain.cpp files. In the first one, we are going to have two classes with avirtual destructor and with 3 members each having a non-virtual accessor. In the second example, the same class will have those accessors turned intovirtual ones. I know it's not a particularly elaborate example, but it's a start.

// main-single-virtual.cpp#include<array>classSingleVirtual{public:SingleVirtual()=default;SingleVirtual(inta,intb,intc):m_a(a),m_b(b),m_c(c){}virtual~SingleVirtual()=default;intgetA()const{returnm_a;}intgetB()const{returnm_b;}intgetC()const{returnm_c;}private:intm_a=0;intm_b=0;intm_c=0;};std::array<SingleVirtual,10'000>a;intmain(){}// main-many-virtuals.cpp#include<array>classManyVirtuals{public:ManyVirtuals()=default;ManyVirtuals(inta,intb,intc):m_a(a),m_b(b),m_c(c){}virtual~ManyVirtuals()=default;virtualintgetA()const{returnm_a;}virtualintgetB()const{returnm_b;}virtualintgetC()const{returnm_c;}private:intm_a=0;intm_b=0;intm_c=0;};std::array<ManyVirtuals,10'000>a;intmain(){}
Enter fullscreen modeExit fullscreen mode

Depending on the optimization level, the version where only the destructor isvirtual was compiled into a binary with the size of281,488/281,520. The fully virtual version had slightly bigger binaries,281,615/281,647.

We can see that the difference is small. In the previous articles, I used an array of 10,000 objects so that we don't have to look into tiny differences. But in this case, it's worth having a look at the exact size of the difference.

It's less than 200 bytes per 10,000 objects. To be more precise, the difference is 127 bytes, way less than one byte per object. It cannot have anything to do with the number of objects. It is only about the size of thevtable. It's almost negligible.

First, I ran both examples with a much smaller array of only 10 objects. The difference between the two versions was still exactly 127 bytes.

As we start to remove thevirtual keywords from the accessors one by one, the difference also shrinks. First by 48 byes to 79 bytes, then by another 32/48 bytes (depending on the optimization level) to 31-47 bytes. And finally, by devirtualizing the third accessor, the difference shrinks by another 48 bytes depending.

Another observation that we can make, is that at the end,ManyVirtuals ended up with a smaller binary. That's because its name is shorter. But that only mattered when the optimization level was-O0

What can we learn from this?

It seems that the size of avtable entry is about 48 bytes, at least on Apple Clang 15. It might not seem a big deal at first, but if we have 10 classes with 3 virtual methods each that's already more than 1KB. But these numbers can be much higher, especially if we consider avirtual method in a class template. That can quickly explode if we don't pay attention.

At the same time, we can only rest assured that the length of class/function names is not a problem anymore as it was in the past - given that we turn on compiler optimizations.

If we have a look at the assembly code after an unoptimized build (so that it remains somewhat readable), we can see things that seem like a list, probably the virtual table:

// SingleVirtual.s    .section    __DATA,__const    .globl  __ZTV13SingleVirtual            ; @_ZTV13SingleVirtual    .weak_def_can_be_hidden __ZTV13SingleVirtual    .p2align    3__ZTV13SingleVirtual:    .quad   0    .quad   __ZTI13SingleVirtual    .quad   __ZN13SingleVirtualD1Ev    .quad   __ZN13SingleVirtualD0Ev// ManyVirtuals.s    .section    __DATA,__const    .globl  __ZTV12ManyVirtuals             ; @_ZTV12ManyVirtuals    .weak_def_can_be_hidden __ZTV12ManyVirtuals    .p2align    3__ZTV12ManyVirtuals:    .quad   0    .quad   __ZTI12ManyVirtuals    .quad   __ZN12ManyVirtualsD1Ev    .quad   __ZN12ManyVirtualsD0Ev    .quad   __ZNK12ManyVirtuals4getAEv    .quad   __ZNK12ManyVirtuals4getBEv    .quad   __ZNK12ManyVirtuals4getCEv
Enter fullscreen modeExit fullscreen mode

ForSingleVirtual, we can see an entry for the constructors and the destructor. We can also see that it's in theconst DATA section. But there is nothing for the getter methods. On the other hand, those are there inManyVirtuals. I couldn't figure out why there are entries for the constructors which are not virtual in C++. If you happen to know, please share in the comments or by e-mail.

Conclusion

In this post, we saw how little it matters whether we add a newvirtual method to a class that has already avirtual destructor. Having avirtual destructor ensures that everything necessary is instrumented for polymorphic behaviour which can make a huge difference in your binary size - and runtime performance. At the same time, adding anothervirtual function barely adds to the size of the binary. It'll only mean an extra entry in yourvtable.

In the next episode, we'll have a look into two classic design patterns. We'll take a classical reference semantic and a modern value semantic implementation and see how much that matters in terms of binary size.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Happy father. Principal Engineer. Author. Creator of dailycppinterview.com
  • Location
    Antibes, France
  • Work
    Senior Software Engineer at Spotify
  • Joined

More fromSandor Dargo

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp