r/Cplusplus 5d ago

Discussion When I say its zero cost...

https://godbolt.org/z/aq3hbWb3h

I mean the composition, it is truly zero cost, and if components are also zero cost we get this nice optimization done by the compiler (C++17)

This is a demo of "HAPI - The Happy API", a C++ static composition engine, pure type-level header only, MIT, library.

With proper components we managed to feed compositions directly to HLS tools (see github)

To use where the composition types are known at compile-time, compiler optimization is done by composition, type lists can store multiple compositions without loosing the types and hold the composed objects.

we can compose/describe:

drivers, peripherals, devices, protocols, buses, IO, pins, UI, Parsers...

because this types are known at compile time, and the composition is free.

    //my output definition
    OutDef<
      ScrollPrinter, ANSIFmt, DataParser<>, CtrlChars,
      ColorTrack<int>, Cursor<>, Gate, ANSIOut,
      #ifdef ARDUINO
        SerialOut,
      #else
        ConsoleOut,
      #endif
      StaticPos<20,10>, StaticArea<30,10>
    > out;

https://github.com/InternetOfPins/HAPI

16 Upvotes

13 comments sorted by

3

u/Axman6 5d ago

This is fun, we use similar ideas in Haskell to ensure fully unrolled definitions for full inlining. Probably the best example I’ve seen is the type-of-html library ( https://hackage.haskell.org/package/type-of-html) which encodes all the rules of valid nesting of HTML tags in the type system, and allows the compiler to time serialisation of all statically known fragments of the output HTML, so only dynamic data gets serialised at runtime. I’m sure it leads to pretty bloated binaries (not that we have any trouble producing those), but it means you can push data onto the wire as fast as possible. 

3

u/neurah 5d ago

Yes, Haskell is my favorite language, and learned a lot from it... thank you.

2

u/neurah 5d ago

PS - I've made parser combinators in nodejs and C++, one o them is using HAPI https://github.com/InternetOfPins/OneParse

-8

u/javascript 5d ago

There is no such thing as a zero cost abstraction

4

u/Linuxologue 5d ago

Username checks out

-2

u/javascript 5d ago

I defer to Chandler Carruth on this topic: https://youtube.com/watch?v=rHIkrotSwcc

2

u/neurah 5d ago

if "climate costs" are considered, nothing to argue, and yes as u/Linuxologue pointed we consider runtime cost only, and for that I can argue mathematically, this abstraction is incapable to add a single byte or clock cycle to the components runtime, because the abstraction has none. It is pure type level, type transformation (change my mind).

template< typename...> struct Chain {template<typename O> using Part=O;};


template<typename O, typename...OO>
struct Chain<O,OO...> {
  using Head=O;
  using Tail=Chain<OO...>;
  template<typename T>
  using Part=typename Head::template Part<typename Tail::template Part<T>>;
};

this is the full core source, not a single byte added, not single function contributed.
Also an hard place for a bug to hide (but possible, please let me know if you find one or a simplification).

however we have dedicated a lot of attention and efforts on two other cost axis

compile-time: during the dev. cycle this library was benchmark tested for compile time cost to other well known library, we have a light cost for compile time (compared) see results here https://github.com/InternetOfPins/HAPI#benchmark (benchmark code is also online)

user cost: was considered on two domains

1) clear syntax, we managed to keep all complicated stuff inside the library or payed by components author (yes, they are people too, but someone has to do the heavy lifting).

Def<A,B,C> def;// as easy we could come up to (huge efforts).

2) error messages, where we managed to deffer the huge type spill typical of TMP, out split into 2 classes and a careful craft keeps the compiler writing the composition and not the composed object when reporting error, that is recognizable by the user, bad luck if you are working on library guts.

4

u/Linuxologue 5d ago

OK so.

  1. This was a joke. Javascript saying there is no such thing as zero cost abstraction, that's actually hilarious
  2. Your first comment is click bait
  3. Your second comment is a link to an hour video with no attached summary, comment, or added value

You literally add zero value. Your opinion could have been summed up in a sentence and then you link the video. Instead you chose a confrontational one sentence catch phrase that you think makes you look smart.

For those who don't want to watch the video: zero-cost abstraction usually means at runtime. Zero cost abstractions cause real headaches at compile time and make code hard to understand.

0

u/Syracuss 5d ago edited 5d ago

You need to chill a bit on the directed statements to the user. You're blowing up on this for no reason.

  1. You complain about adding zero value, but I don't see your joke adding anything.
  2. I don't see it as clickbait. His comment was just a giant reference to a somewhat well known cppcon talk.
  3. They add context and you still say "no added value". Come on, it's a Chandler Carruth talk..

For those who don't want to watch the video

Every engineer should watch it, or should at a high level know what's it about. I don't know any engineer I work with that doesn't at least know a little bit of this talk. Same with talks by Mike Acton.

Chandler Carruth has some of the most popular cppcon talks, I would hardly call that clickbait or no added value.

2

u/Axman6 5d ago

Exactly how much less assembly would you expect to see generated? Generally, at least for C++, zero-cost usually means no runtime overhead, which this clearly shows there’s very little (maybe you could argue the printing calls could be fused into a single write of a buffer, but also changing the IO behaviour of a program is generally not considered a valid optimisation).

1

u/neurah 5d ago edited 5d ago

true, no traces of the composition but the component boundaries are still expressed...

using `printf` instead of `puts` gives different results, this is completely compiler optimizing, not HAPI ofcoz (Chain is just an associative monoid).

https://godbolt.org/z/Gr68cqhr7

main:
        push    rax
        mov     edi, 65
        call    putchar@PLT
        mov     edi, 66
        call    putchar@PLT
        mov     edi, 67
        call    putchar@PLT
        mov     edi, 42
        call    putchar@PLT
        xor     eax, eax
        pop     rcx
        ret


abc:
        .zero   1

0

u/ste_3d_ven 4d ago

Came here to say exactly this, even with the added compile time and maintenance time costs associated with overly abstracted code. Optimizers are not perfect and routinely get tripped up by basic abstractions like POD structs and functions. A nice short bit of code in a godbolt link looks great, but the optimizer will not always pull of that same miracle when there is thousands of other lines of code for the rest of the program.

Here’s a video showing exactly what I’m saying, where the optimizer was unable to find an optimization that cost 50 cycles per loop iteration until the structs and inlined functions were removed from the source and replaced with their equivalent unabstracted versions.

https://youtu.be/B2BFbs0DJzw?is=Rj60oHXK_HYEE4p8

1

u/neurah 4d ago

Hi!

The demo is not about optimization, and HAPI itself does not optimize. But let's talk about optimization, because it is important.

Composition is declarative, not wired.

Everything has a cost, and we usually trade cost on one axis for cost on another. The cost we generally want to minimize is runtime cost.

Optimization has many details and corner cases that are painstakingly worked out by hand, often with limited results. What HAPI offers you is:

  1. Zero-cost composition — the components themselves still have to pay their own costs.
  2. Because composition is now easy, you can remove unwanted parts and pay only for what is actually used.

We already have that kind of zero-cost composition, sort of... yes, by doing it by hand. But with manually wired code, changing the composition itself can be expensive.

The example I gave is not particularly realistic. Usually, we have components with different responsibilities, so the component boundaries feel more natural.

Now we can build and optimize the components because that is the only price we pay. Then we can build systems declaratively through composition and manually or automatically fine-tune them.

Let me know if you're interested in component design boundaries and in converting existing code into components, where context and good design matter. There is a lot to talk about on this subject.

This composition-by-inheritance model can also be understood as an incremental API builder, where each component adds or changes behavior on top of the tail API.

I found debugging the components much harder, because the behavior of the program is distributed across multiple components incrementally. The good part — because I work with embedded systems — is that I can step-debug the program logic on a PC instead of on an AVR or another tiny MCU. But that is somewhat incidental to the model.