29

Personally, I quite like header-only libraries, but there are claims they cause code bloat due to over-inlining (as well as the other obvious problem of longer compile times).

I was wondering, how much truth is there to these claims (the one about bloat)?

Furthermore, are the costs 'justified'? (Obviously there are unavoidable cases such as when it's a library implemented purely or mostly with templates, however I'm more interested in the case where there's actually a choice available.)

I know there's no hard and fast rule, guideline, etc as far as stuff like this goes, but I'm just trying to get a feel for what others think on the issue.

P.S. Yes this is a very vague and subjective question, I'm aware, and so I have tagged it as such.

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
RaptorFactor
  • 2,810
  • 1
  • 29
  • 36
  • Related: https://stackoverflow.com/questions/2174657/when-are-header-only-libraries-acceptable | https://softwareengineering.stackexchange.com/questions/305618/are-header-only-libraries-more-efficient – Ciro Santilli OurBigBook.com Aug 28 '20 at 16:50
  • It's interesting how C++ has changed since this question (and questions like it). Link Time Optimization is more of a possibility now, for some, and the inline keyword no longer practically has influence over whether compilers inline a function. And, not sure if compilers have generally improved but it seems as if concerns over the compiler inlining too much aren't such a thing anymore. – thomasrutter Aug 18 '21 at 22:11

4 Answers4

9

In my experience bloat hasn't been a problem:

  • Header only libraries give compilers greater ability to inline, but they do not force compilers to inline - many compilers treat the inline keyword as nothing more than a command to ignore multiple identical definitions.

  • Compilers usually have options to optimize to control the amount of inlining; /Os on Microsoft's compilers.

  • It's usually better to allow the compiler to manage the speed vs. size issues. You'll only see bloat from calls that have actually been inlined, and the compiler will only inline them if its heuristics indicate that it inlining will improve performance.

I wouldn't consider code bloat as a reason to stay away from header only libraries - but I would urge you to consider how much a header only approach will increase compile times by.

JoeG
  • 12,994
  • 1
  • 38
  • 63
8

I work for a company that has a "Middleware" department of its own to maintain a few hundreds of libraries that are commonly used by a great many teams.

Despite being in the same company, we shy from header only approach and prefer to favor binary compability over performance because of the ease of maintenance.

The general consensus is that the performance gain (if any) would not be worth the trouble.

Furthermore, the so called "code-bloat" may have a negative impact on performance as more code to be loaded in the cache implies more cache miss, and those are the performance killers.

In an ideal world I suppose that the compiler and the linker could be intelligent enough NOT to generate those "multiple definitions" rules, but as long as it is not the case, I will (personally) favor:

  • binary compatibility
  • non-inlining (for methods that are more than a couple of lines)

Why don't you test ? Prepare the two libraries (one header only and the other without inlining methods over a couple of lines) and check their respective performance in YOUR case.

EDIT:

It's been pointed out by 'jalf' (thanks) that I should precise what I meant exactly by binary compatibility.

2 versions of a given library are said binary compatible if you can (usually) link against one or the other without any change of your own library.

Because you can only link with one version of a given library Target, all the libraries loaded that use Target will effectively use the same version... and here is the cause of the transitivity of this property.

MyLib --> Lib1 (v1), Lib2 (v1)
Lib1 (v1) --> Target (v1)
Lib2 (v1) --> Target (v1)

Now, say that we need a fix in Target for a feature only used by Lib2, we deliver a new version (v2). If (v2) is binary compatible with (v1), then we can do:

Lib1 (v1) --> Target (v2)
Lib2 (v1) --> Target (v2)

However if it's not the case, then we will have:

Lib1 (v2) --> Target (v2)
Lib2 (v2) --> Target (v2)

Yep, you read it right, even though Lib1 did not required the fix, you head to rebuild it against a new version of Target because this version is mandatory for the updated Lib2 and Executable can only link against one version of Target.

With a header-only library, since you don't have a library, you are effectively not binary compatible. Therefore each time you make some fix (security, critical bug, etc...) you need to deliver a new version, and all the libraries that depend on you (even indirectly) will have to be rebuilt against this new version!

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 3
    How does header only libs imply code bloat? The compiler generally won't inline if it means significantly larger code. And, for that matter, how is binary compatibility affected by header-only libs? – jalf Feb 01 '10 at 12:01
  • @Matthieu: Testing is indeed always good to determine which would yield the best results. – DrYak Feb 01 '10 at 12:09
  • 1
    @Jalf: *** 1. Header-only scenario: for every single change to the library an update to the library means recompiling every last project having dependency on it. (Think of Matthieu's situation : hundreds of libraries, lots of teams - lots of recompiling involved). *** 2. binary only scenario : for lots of different changes, an update will simply involve dropping a new .SO or .DLL file, as long as the ABI remains the same. Though new feature could change method signatures and data structures, critical updates (security or stability) seldom do. – DrYak Feb 01 '10 at 12:17
  • 1
    True about recompiling, but that has nothing to do with either code bloat or binary compatibility. – jalf Feb 01 '10 at 12:23
  • @jalf: inline changes the link sematincs. Simple-minded compilers **do** generate the method bodies for each translation unit, and the linker sometimes/often fails to fold the duplicates. – peterchen Feb 01 '10 at 12:25
  • @peterchen: Got any concrete examples of a linker actually failing to do that? First I've ever heard of it, and seems like it'd be a clear violation of C++'s semantics. – jalf Feb 01 '10 at 13:10
  • To elaborate, I'm pretty sure the standard guarantees that a function has one and only one address. If it varies arbitrarily depending on which translation unit it is called from, it could break quite a bit of existing code. – jalf Feb 01 '10 at 13:29
  • @jalf: A function does have only one address, as long as it is not inlined (in which case it disappears). Now what happens if you use a component that depends on a "binary compatible" version of the header only library... but with a function whose internals are different from the one you do ? Well, let us pray it is not inlined... – Matthieu M. Feb 01 '10 at 16:38
  • @Matthieu: The "only one address" was a reference to @peterchen saying you couldn't rely on the linker to merge multiple function definitions together if they're defined as `inline`. You're right, if you start allowing different parts of the code to see different *versions* of a function, that's a clear ODR violation and you're in for some fun debugging time. But that can happen with or without header-only libs and assumes that code isn't recompiled when the headers it depends on are changed. I get what you're trying to say, but I disagree that it's a problem inherent to header-only libs. – jalf Feb 01 '10 at 17:02
  • @jalf: You are right, it's not supposed to be this way. I remember some discussions around WATCOM C/C++ (10 or even earlier), however I have no links, back then internet access was a rare occurence for me. I do remember an extensive description of the search for the reason of code bloat - turned out to be std:.string. – peterchen Feb 01 '10 at 20:00
  • I'd say that for *normal* code, there's little reason to worry about ancient non-compliant compilers. Of course early C++ compilers messed up in all sorts of creative ways, but that hardly means it's going to be a problem today, using a modern compiler – jalf Feb 01 '10 at 20:04
  • @jalf: my comment was for the "binary compatibility" issue. The problem is that this violation of ODR means that no "header-only" library can deliver a "binary compatible" version to fix a bug. You can only deliver a "source compatible" version which forces every single dependency to be recompiled against the new version and clearly marked as not binary compatible either. So not only do can't you deliver "binary compatible" upgrades for THIS library, but you also impact DEPENDENT libraries in an avalanche effect! – Matthieu M. Feb 02 '10 at 08:44
  • @Matthieu: Put that in your answer then. You're absolutely right, but your answer gave no context for the "no binary compatibility" argument. Binary compatibility is only an issue if you change code that affects multiple libraries, without actually recompiling all the affected libraries, which is an error. What you're describing is less to do with binary compatiblity than with modularity or maintainability. With header-only lib the same code is compiled into multiple libs, and so multiple libs have to be recompiled when that code changes. – jalf Feb 02 '10 at 14:32
  • True, it was so evident for me because I deal daily with it that I did not thought further explanations were needed... sorry about that. – Matthieu M. Feb 02 '10 at 16:35
  • But with template, we can only write code in a header-only way. And template is like a holy grail if you don't want to repeat yourself... – cyfex Aug 20 '23 at 06:43
3

I agree, inline libraries are much easier to consume.

Inline bloat mostly depends on the development platform you are working with - specifically, on the compiler / linker capabilities. I wouldn't expect it to be a major problem wiht VC9 except in a few corner cases.

I've seen some notable change in final size in some places of a large VC6 project, but it's hard to give a specific "acceptable, if...". You probalby need to try with your code in your devenv.

A second problem may be compile times, even when using precompiled header (there are tradeoffs, too).

Third, some constructs are problematic - e.g. static data members shared across translation units - or avoiding having a separate instance in each translation unit.


I have seen the following mechanism to give the user a choice:

// foo.h
#ifdef MYLIB_USE_INLINE_HEADER
#define MYLIB_INLINE inline
#else 
#define MYLIB_INLINE 
#endif

void Foo();  // a gazillion of declarations

#ifdef MYLIB_USE_INLINE_HEADER
#include "foo.cpp"
#endif

// foo.cpp
#include "foo.h"
MYLIB_INLINE void Foo() { ... }
peterchen
  • 40,917
  • 20
  • 104
  • 186
1

Over-inlining is probably something that should be addressed by the caller, tuning their compiler options, rather than by the callee trying to control it through the very blunt instruments of the inline keyword and definitions in headers. For example GCC has -finline-limit and friends, so you can use different inlining rules for different translation units. What's over-inlining for you may not be over-inlining for me, depending on architecture, instruction cache size and speed, how the function is used, etc. Not that I've ever needed to do this tuning: in practice when it has been worth worrying about that, it has been worth rewriting, but that could be coincidence. Either way, if I'm the user of a library then all else being equal I'd rather have the option to inline (subject to my compiler, and which I might not take up) than be unable to inline.

I think the terror of code bloat from header-only libraries comes more from the worry that the linker won't be able to remove redundant copies of code. So regardless of whether the function is actually inlined at call sites or not, the concern is that you end up with a callable copy of the function (or class) per object file that uses it. I can't remember whether addresses taken to inline functions in different translation units in C++ have to compare equal, but even assuming they do, so that there is one "canonical" copy of the function in the linked code, it doesn't necessarily mean the linker will actually remove the dead duplicate functions. If the function is defined in only one translation unit, you can be reasonably confident there will only be one stand-alone copy per static library or executable that uses it.

I honestly don't know how well-grounded this fear is. Everything I've worked on has either been so tightly memory constrained that we've used inline only as static inline functions so small that we don't expect the inlined version to be noticeably bigger than the code to make a call, and don't mind duplicates, or else so loosely constrained that we don't care about any duplicates anywhere. I've never yet hit the middle ground of looking for and counting duplicates on various different compilers. I've occasionally heard from others that it's been a problem with template code, though, so I believe that there is truth in the claims.

Making this up as I go along now, I think if you ship a header-only library, the user can always mess with it if they don't like it. Write a new header which declares all the functions and a new translation unit which includes the definitions. Functions defined in classes will have to be moved to external definitions, so if you want to support this use without requiring the user to fork your code, you could avoid doing that and supply two headers:

// declare.h
inline int myfunc(int);

class myclass {
    inline int mymemberfunc(int);
};

// define.h
#include "declare.h"
int myfunc(int a) { return a; }

int myclass::mymemberfunc(int a) { return myfunc(a); }

Callers who are worried about code bloat can probably outwit their compiler by including declare.h in all their files, then writing:

// define.cpp
#include "define.h"

They probably also need to avoid whole-program optimisation to be sure the code won't be inlined, but then you can't be sure that even a non-inline function won't be inlined by whole-program optimisation.

Callers who aren't worried about code bloat can use define.h in all their files.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699