I run these cases against an array of optimizations I'm most interested in: -O0, g, s, 2 and 3. The graphs I post has left, runtime in cycles, and right, image size in bytes.
Nice setup.
You missed my default, -O aka -O1. I've never included -Og in my own comparisons, but I think -O does do some size/speed optimisations that -Og won't. But I don't have anything against -O2 and take your point about C++ devirtualisation.
Thanks.
Yes, I did spot the accidental omission of O1 too. I will add it later. Running a single sweep of all tests may take a few minutes, but for a (home)server thats otherwise idling all day its no big deal.
I'm also at a point where I may need to include more instruction extensions, like floating point and compressed instructions. I'll have to see how I can plot it in such a way the data is still useful.
I've sometimes found, especially on RISC-V, that the -Os heuristics aren't all that well tuned -- especially because gcc is NOT aware of the C extension -- that you can surprisingly often end up with fewer instructions but larger code than -O2. But you won't see that effect with rv32i or rv32im.
I've seen this behaviour too
without the C extension with RISC-V GCC 13.X as well. The result I showed is from an older compiler version. Not sure why this is exactly though.
I'm not aware of any commercial microcontrollers that don't have the C extension (even the 10c CH32V003 is rv32ec), so I guess you're using an FPGA soft core? It gives more code size reduction than -Os with less (or zero) speed cost.
Its an emulator I wrote myself in Python which I did as a preparation for becoming familiar with writing a FPGA soft-core. Unfortunately, the soft-core is still a TODO, but the project's utility was quite good so far.
The emulator supports rv32im, recently added a subset of atomic (RTOS work), and also some coprocessor extensions (machine interrupts)
I've some basic peripherals like timers, plus a special emulator peripheral which can interact with the emulator environment.
For example, instrumentation time start/stop, enabling/disabling a full instruction trace for hotpoints in my code, etc.
The emulator has a few debugging features such as simply crashing instruction stream outright on a failed instruction (or infinite loops), which then gives a triggered PC location + C line location (taken from .elf debug symbols). The instruction trace also contains this info, as well as manipulated register values. Tracking down which instruction or line of code caused a null pointer dereference becomes a lot easier.
There is also a "delayed" soft interrupt. Basically you write a delay count to a peripheral register, and it will count down to zero (at CPU clock rate) and fire an interrupt. This way I can sweep a range of cycle counts through function calls that are supposed to be reentrant. Again, super useful for low-level work.
The only thing I'm missing sometimes is GDB stepping through code (sifting through 1GiB traces is not the first thing I would do).. but unit testing higher level code on x86 helps a ton.
Another wishlist thing: more speed. Python is nice in quickly developing tools like this, but it only runs binaries at 50-100kHz. I wrote a basic C++ version that ran at 50MHz, but unfortunately not all these instruction tracing and benchmarking features are included yet.
The copying in/out of the object type is done via the assignment operator. In -Os, this will typically be fine, but then when I test a 17-byte object I get a bytewise memcpy that is very very slow. Even -Og is fine with this, so -Os shows a massive regression in performance.
Is this simply the result of using packed structures?
I don't think an attribute packed in my code.. but it appears very much like so.
Perhaps GCC decided that a memcpy call is smaller than 5 loads + 5 stores, and that is what it prioritized.