[Development] Qt binaries with inlined functions

Thiago Macieira thiago.macieira at intel.com
Tue Apr 29 21:22:16 CEST 2014


Em ter 29 abr 2014 08:08:35 vocĂȘ escreveu:
> Thiago,
> 
> Thanks for that piece. I am not that familiar with such details of C++ so
> could you please explain why the function is public but compiled with
> hidden symbols?

[with permission to reply to the list]

There are two things here at stake: 1) inline functions; 2) hidden symbols. 
Inline functions are part of the C++ language; deciding how to inline 
functions and how to export symbols is part of the specific ABI. I'm replying 
referring to the portable C++ ABI, a.k.a. the IA-64 C++ ABI. MSVC behaves 
similarly for most aspecs, but not totally. It's got some violations of the 
C++ language due to the way it does inlining, so I'm going to ignore it and 
hope its ABI dies (which won't happen).

> On Tuesday, April 29, 2014 6:03 PM, Thiago Macieira
> <thiago.macieira at intel.com> wrote:
> Still hidden:
> 
> $ readelf -Ws --dyn-syms libQt5Core.so.5 | c++filt| grep QModelIndex::row
> 14630: 000000000028bdf6    16 FUNC    LOCAL  HIDDEN    12 QModelIndex::row()
> const

By the way, on the above, note that it matched the symbol in the "static" 
symbol table, not the dynamic one:

$ readelf -W --dyn-syms libQt5Core.so | c++filt| grep QModelIndex::row
$ readelf -W --syms libQt5Core.so | c++filt| grep QModelIndex::row 
 14630: 000000000028bdf6    16 FUNC    LOCAL  HIDDEN    12 QModelIndex::row() 
const

The static symbol table is stripped from the library by the /usr/bin/strip 
command along with debug symbols; the dynamic symbol table stays there.

=== Inlines ===

A function is "inline" if it's either declared with the inline keyword or if 
its body is inside the class/struct/union body.

A function gets *inlined* if the compiler thinks it's good idea. Any function 
whose body is visible at compilation time can be inlined, even functions that 
aren't inline. What's more, functions marked "inline" may not get inlined if 
the compiler doesn't think it's a good idea. In particular, without 
optimisations (-O0, but also with -fno-inline), no functions are inlined.

So the question is: how does the compiler resolve that issue?

A symbol for each function that is *not* inline must always be emitted. If I 
have my sources as:

	void f() { somethingElse(); }
	void g() { f();}

The smart compiler will realise f() simply calls somethingElse() and will 
inline it in g()'s body. However, since the function was not declared as 
inline, the compiler will generate a full function f() and emit its symbol.

However, if the function is declared inline, the compiler need not emit it:

	inline void f() { somethingElse(); }
	void g() { f();}

If you inspect the .o file, you're going to see g(), but no f() for the above.

Now suppose that the compiler decides not to inline that inline function 
above, what will it do? A good example is QString's constructor: it's inline 
from qstring.h and almost every single .cpp will call it. 

Here's the interesting thing: the compiler must *always* emit the required 
code of all inline functions it used. It cannot assume that another .o will 
provide it. But we don't want to bloat the binary because of that.

The compiler will emit the function f() but it will mark it as "merge at link 
time" (nm shows it with type "V"). That way, all out-of-line copies of inline 
functions get merged at link-time.

What's more, the dynamic linker does the same again: if two functions have the 
same name, the dynamic linker will resolve to only one of the copies. That 
allows us to take address of functions in two different ELF modules and still 
compare equally.

=== Visibility ===

Now enter visibility. There are four visibility types in ELF: default, 
protected, hidden, internal. Mach-O has two visibility types: default and 
hidden. COFF-PE has three: dllimport, dllexport ("protected") and none 
("hidden"). There's no direct equivalent to dllimport (it's similar to 
"default" but not entirely). The types default and protected are visible: 
other modules can see the symbol and link to them; types hidden and internal 
are invisible: other modules do not see them at all.

When we're compiling statically, visibility does not apply. That means all the 
Q_LIBNAME_EXPORT macros expand to empty, on all platforms.

When we're compiling dynamically, we switch between Q_DECL_EXPORT (dllexport, 
default or protected) and Q_DECL_IMPORT (dllimport or default). Q_DECL_EXPORT 
is used when we're compiling the library that should contain the symbol, 
whereas Q_DECL_IMPORT should be used when just using the symbol from another 
library. The names should be self-explanatory.

We use visibility to control what symbols get exported for other libraries to 
see. We do not export private symbols and internal classes. This decreases 
dramatically the symbol table in Qt, allowing for faster link times and load 
times.

=== Visibility of inlines ===

The C++ standard requires the each function be defined only once. It's illegal 
(undefined behaviour) for a function to have two different bodies, be it by way 
of #defines, different #includes or include paths, or by upgrading and 
recompiling.

Since an inline function may be inlined in many different modules, it exists in 
possibly a lot of different copies, inlined and out-of-line. So a simple 
decision is taken: instead of resolving all calls to one single out-of-line 
copy, we tell the compiler to always resolve to the copy that the current 
module carries. This again reduces the symbol table and improves performance, 
since the linker can resolve the call to a simple PC-relative jump, as opposed 
to an indirect call through a PLT gate.

This technically will violate ODR: if you take the address of an inline 
function in two different modules and compare, they will compare differently.

-- 
Thiago Macieira - thiago.macieira (AT) intel.com
  Software Architect - Intel Open Source Technology Center




More information about the Development mailing list