https://youtu.be/HKlnn0hd8J0
Bugs I’ve Seen in the Wild - From Confusion to Amazement - Olivier Petit - [https://youtu.be/LBWtb\_uXt0I](https://youtu.be/LBWtb_uXt0I)
Real-Time Audio in Python: Introducing the asmu Package - Felix Huber - https://youtu.be/X2vr81CJ934
2026-06-29 - 2026-07-05
Beyond iLok: Advanced Code Protection and Cryptography for the Next Generation - Protecting the Next Generation of Applications, Plug-ins, and AI Models - Neal Michie, Ryan Wardell & Bob Brown - [https://youtu.be/dbbK\_ry2cgo](https://youtu.be/dbbK_ry2cgo)
Database Synchronisation for Audio Plugins, Part Two - Here's One I Made Earlier - Adam Wilson - https://youtu.be/wJCy2G969ro
Perfect Oscillators in Less Than One Clock Cycle - Angus Hewlett - [https://youtu.be/Ssq0a-YdamM](https://youtu.be/Ssq0a-YdamM)
Driving Chaos - Virtual Analog Modelling of a Chaotic Circuit with Wave Digital Filters - Francisco Bernardo - https://youtu.be/PnEZNqyKlIw
Boost Documentary
There is also a teaser trailer for a new documentary on the history of the Boost C++ library https://www.youtube.com/watch?v=87jvuDbnwqQ which will have its first showing at CppCon this year
https://redd.it/1v83trq
@r_cpp
Bugs I’ve Seen in the Wild - From Confusion to Amazement - Olivier Petit - [https://youtu.be/LBWtb\_uXt0I](https://youtu.be/LBWtb_uXt0I)
Real-Time Audio in Python: Introducing the asmu Package - Felix Huber - https://youtu.be/X2vr81CJ934
2026-06-29 - 2026-07-05
Beyond iLok: Advanced Code Protection and Cryptography for the Next Generation - Protecting the Next Generation of Applications, Plug-ins, and AI Models - Neal Michie, Ryan Wardell & Bob Brown - [https://youtu.be/dbbK\_ry2cgo](https://youtu.be/dbbK_ry2cgo)
Database Synchronisation for Audio Plugins, Part Two - Here's One I Made Earlier - Adam Wilson - https://youtu.be/wJCy2G969ro
Perfect Oscillators in Less Than One Clock Cycle - Angus Hewlett - [https://youtu.be/Ssq0a-YdamM](https://youtu.be/Ssq0a-YdamM)
Driving Chaos - Virtual Analog Modelling of a Chaotic Circuit with Wave Digital Filters - Francisco Bernardo - https://youtu.be/PnEZNqyKlIw
Boost Documentary
There is also a teaser trailer for a new documentary on the history of the Boost C++ library https://www.youtube.com/watch?v=87jvuDbnwqQ which will have its first showing at CppCon this year
https://redd.it/1v83trq
@r_cpp
YouTube
An Open Toolkit for Real-Time Audio Descriptors - Valerio Orlandini - ADC 2025
https://audio.dev/ -- @audiodevcon
ADC Bristol - 9th - 11th November
---
An Open Toolkit for Real-Time Audio Descriptors - Valerio Orlandini - ADC 2025
---
In modern audio applications, from music production and interactive installations to scientific…
ADC Bristol - 9th - 11th November
---
An Open Toolkit for Real-Time Audio Descriptors - Valerio Orlandini - ADC 2025
---
In modern audio applications, from music production and interactive installations to scientific…
AVX-512 Intrinsics Optimization: Achieving ~7% speedup over libpopcnt on Sapphire Rapids
Benchmarking random 64B cache-line jumps on Intel Xeon Sapphire Rapids showed the custom kernel achieving 7.09 ns/line, compared to
While contiguous memory performance ties
Target is v40. Looking for insights on whether software prefetching (
https://redd.it/1v86xcs
@r_cpp
Benchmarking random 64B cache-line jumps on Intel Xeon Sapphire Rapids showed the custom kernel achieving 7.09 ns/line, compared to
libpopcnt (7.58 ns) and std::popcount (46.10 ns).While contiguous memory performance ties
libpopcnt at 0.47 ns/word, the performance gain on this gather hot-path comes from maintaining accumulator states inside ZMM registers across steps and applying 2-accumulator unrolling to break dependency chains.Target is v40. Looking for insights on whether software prefetching (
_mm_prefetch) provides measurable benefits for this gather pattern or if latency is memory-bound.https://redd.it/1v86xcs
@r_cpp
Reddit
From the cpp community on Reddit
Explore this post and more from the cpp community
Announcing Ada v4: Validating 35.6M URLs per second
https://www.yagiz.co/release-of-ada-v4
https://redd.it/1v8hxxn
@r_cpp
https://www.yagiz.co/release-of-ada-v4
https://redd.it/1v8hxxn
@r_cpp
Yagiz Nizipli's blog
Announcing Ada v4: Validating 35.6M URLs per second
Ada 4.0 is faster on the common path, ships a much smaller binary, and hardens URL parsing with max-length limits, ABI soname 4, and a wave of correctness fixes.
Ray Tracing in One Weekend in Metal
Hi! I recently implemented Ray Tracing in One Weekend in Metal using metal-cpp. Resources for metal-cpp specifically are pretty sparse, so I figured I'd share in case it helps anyone getting started with it or with graphics programming in general. Hope it helps!
Repo: https://github.com/jinhgkim/Path-Tracer
https://redd.it/1v8kbu7
@r_cpp
Hi! I recently implemented Ray Tracing in One Weekend in Metal using metal-cpp. Resources for metal-cpp specifically are pretty sparse, so I figured I'd share in case it helps anyone getting started with it or with graphics programming in general. Hope it helps!
Repo: https://github.com/jinhgkim/Path-Tracer
https://redd.it/1v8kbu7
@r_cpp
GitHub
GitHub - jinhgkim/Path-Tracer: A GPU-accelerated Ray Tracing in One Weekend in Apple Metal
A GPU-accelerated Ray Tracing in One Weekend in Apple Metal - jinhgkim/Path-Tracer
A Design Study for a Macro-Free Testing Library
https://jonastoth.github.io/posts/rtest_library/
https://redd.it/1v8qw4e
@r_cpp
https://jonastoth.github.io/posts/rtest_library/
https://redd.it/1v8qw4e
@r_cpp
jonastoth.github.io
rtest - A C++-26 reflection based testing library
Writing software tests in C++ is commonly done through macro-based test definitions. This can change with C++-26 reflection and the `rtest` library is a design …
ODB C++ ORM version 2.6.0 released
https://codesynthesis.com/pipermail/odb-announcements/2026/000047.html
https://redd.it/1v8ycqt
@r_cpp
https://codesynthesis.com/pipermail/odb-announcements/2026/000047.html
https://redd.it/1v8ycqt
@r_cpp
Constraints of mdspan policy layout_stride
Wtith great support of Mark Hoemmen and Christian Robert Trott (two authors of std::mdspan), I just finished the mdspan chapter of "C++23 - The Complete Guide" (https://www.cppstd23.com/). I learned something I was not aware of and what is not obvious:
If you specify a layout\_stride mapping, there are several constraints you have to take into account. Otherwise, the resulting code has undefined behavior.
The constaintes are:
* Each layout mapping must be ***unique***. This means that each underlying element may only be reached by one combination of indices. In other words: subsets of this layout may not overlap and iterating over all elements may not visit any element twice.
* Only ***positive offsets*** from one stride to the other are allowed. This, for example, means that you cannot use this layout for direct reverse iterations.
* There must be one way to perform a multi-dimensional iteration so that the ***resulting offsets*** to the underlying memory are **ascending.**
For example:
* [https://www.cppstd23.com/code/mdspan/mdspanstride2.cpp.html](https://www.cppstd23.com/code/mdspan/mdspanstride2.cpp.html) is valid.
* [https://www.cppstd23.com/code/mdspan/mdspanstrideUB4.cpp.html](https://www.cppstd23.com/code/mdspan/mdspanstrideUB4.cpp.html) has undefined behavior.
Hope this helps.
https://redd.it/1v8zynw
@r_cpp
Wtith great support of Mark Hoemmen and Christian Robert Trott (two authors of std::mdspan), I just finished the mdspan chapter of "C++23 - The Complete Guide" (https://www.cppstd23.com/). I learned something I was not aware of and what is not obvious:
If you specify a layout\_stride mapping, there are several constraints you have to take into account. Otherwise, the resulting code has undefined behavior.
The constaintes are:
* Each layout mapping must be ***unique***. This means that each underlying element may only be reached by one combination of indices. In other words: subsets of this layout may not overlap and iterating over all elements may not visit any element twice.
* Only ***positive offsets*** from one stride to the other are allowed. This, for example, means that you cannot use this layout for direct reverse iterations.
* There must be one way to perform a multi-dimensional iteration so that the ***resulting offsets*** to the underlying memory are **ascending.**
For example:
* [https://www.cppstd23.com/code/mdspan/mdspanstride2.cpp.html](https://www.cppstd23.com/code/mdspan/mdspanstride2.cpp.html) is valid.
* [https://www.cppstd23.com/code/mdspan/mdspanstrideUB4.cpp.html](https://www.cppstd23.com/code/mdspan/mdspanstrideUB4.cpp.html) has undefined behavior.
Hope this helps.
https://redd.it/1v8zynw
@r_cpp
Cppstd23
Nicolai M. Josuttis: C++23 - The Complete Guide
Programming with C++23 by Nicolai Josuttis
Latest News From Upcoming C++ Conferences (2026-07-28)
**TICKETS AVAILABLE TO PURCHASE**
The following conferences currently have tickets available to purchase
* **CppCon (12th – 18th September)** – You can buy standard tickets until August 29th at [https://cppcon.org/registration/](https://cppcon.org/registration/)
* **C++ Under The Sea** **(14th – 16th October)** – You can buy early bird tickets at [https://sales.ticketing.cm.com/cppunderthesea2026/](https://sales.ticketing.cm.com/cppunderthesea2026/)
* **ADC – (9th – 11th November)** – Tickets for ADC can now be purchased at [https://ti.to/audio-developer-conference/adc-bristol-2026](https://ti.to/audio-developer-conference/adc-bristol-2026)
* **Meeting C++ (26th – 28th November)** – You can buy early bird tickets at [https://meetingcpp.com/2026/](https://meetingcpp.com/2026/)
**OPEN CALL FOR SPEAKERS**
**OTHER OPEN CALLS**
* **(Last Chance) CppCon Call For Volunteers Now Open** – Interested volunteers have until August 1st to apply at the CppCon main conference which is scheduled to take place from 14th – 18th September. For more information including how to apply visit [https://cppcon.org/cfv2026/](https://cppcon.org/cfv2026/)
**TRAINING COURSES AVAILABLE FOR PURCHASE**
Conferences are offering the following training courses:
**CppCon Online Workshops**
**9th – 11th September**
1. **Modern C++: When Efficiency Matters** – Andreas Fertig – 3 day online workshop available on 9th – 11th September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-when-efficiency-matters/](https://cppcon.org/class-2026-when-efficiency-matters/)
2. **System Architecture And Design Using Modern C++** – Charley Bay – 3 day online workshop available on 9th – 11th September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-system-architecture-and-design-using-modern-cpp/](https://cppcon.org/class-2026-system-architecture-and-design-using-modern-cpp/)
**21st – 23rd September**
1. **C++ Fundamentals You Wish You Had Known Earlier** – Mateusz Pusz – 3 day online workshop available on 21st– 23rd September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-cpp-fundamentals/](https://cppcon.org/class-2026-cpp-fundamentals/)
2. **C++23 in Practice: A Complete Introduction** – Nicolai Josuttis – 3 day online workshop available on 21st– 23rd September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-cpp23-in-practice/](https://cppcon.org/class-2026-cpp23-in-practice/)
3. **Programming with C++20** – Andreas Fertig – 3 day online workshop available on 21st– 23rd September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-programming-with-cpp20/](https://cppcon.org/class-2026-programming-with-cpp20/)
**26th – 27th September**
1. **Using C++ for Low-Latency Systems** – Patrice Roy – 2 day online workshop available on 26th– 27th September 09.00 – 17.00 MDT – [https://cppcon.org/class-2026-low-latency/](https://cppcon.org/class-2026-low-latency/)
This is the latest news from upcoming C++ Conferences. You can review all of the news at [https://programmingarchive.com/upcoming-conference-news/](https://programmingarchive.com/upcoming-conference-news/)
**CppCon Onsite Workshops**
All onsite workshops will take place in the Gaylord Rockies in Aurora, Colorado
**12th & 13th September**
1. **Advanced and Modern C++ Programming: The Tricky Parts** – Nicolai Josuttis – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-tricky-parts/](https://cppcon.org/class-2026-tricky-parts/)
2. **C++ Best Practices** – Jason Turner – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-best-practices/](https://cppcon.org/class-2026-best-practices/)
3. **How Hardware Gets Hacked: Breaking and Defending Embedded Systems** – Nathan Jones – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-hardware-hack/](https://cppcon.org/class-2026-hardware-hack/)
4. **Mastering \`std::execution\`: A Hands-On Workshop** – Mateusz Pusz – 2 day in-person
**TICKETS AVAILABLE TO PURCHASE**
The following conferences currently have tickets available to purchase
* **CppCon (12th – 18th September)** – You can buy standard tickets until August 29th at [https://cppcon.org/registration/](https://cppcon.org/registration/)
* **C++ Under The Sea** **(14th – 16th October)** – You can buy early bird tickets at [https://sales.ticketing.cm.com/cppunderthesea2026/](https://sales.ticketing.cm.com/cppunderthesea2026/)
* **ADC – (9th – 11th November)** – Tickets for ADC can now be purchased at [https://ti.to/audio-developer-conference/adc-bristol-2026](https://ti.to/audio-developer-conference/adc-bristol-2026)
* **Meeting C++ (26th – 28th November)** – You can buy early bird tickets at [https://meetingcpp.com/2026/](https://meetingcpp.com/2026/)
**OPEN CALL FOR SPEAKERS**
**OTHER OPEN CALLS**
* **(Last Chance) CppCon Call For Volunteers Now Open** – Interested volunteers have until August 1st to apply at the CppCon main conference which is scheduled to take place from 14th – 18th September. For more information including how to apply visit [https://cppcon.org/cfv2026/](https://cppcon.org/cfv2026/)
**TRAINING COURSES AVAILABLE FOR PURCHASE**
Conferences are offering the following training courses:
**CppCon Online Workshops**
**9th – 11th September**
1. **Modern C++: When Efficiency Matters** – Andreas Fertig – 3 day online workshop available on 9th – 11th September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-when-efficiency-matters/](https://cppcon.org/class-2026-when-efficiency-matters/)
2. **System Architecture And Design Using Modern C++** – Charley Bay – 3 day online workshop available on 9th – 11th September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-system-architecture-and-design-using-modern-cpp/](https://cppcon.org/class-2026-system-architecture-and-design-using-modern-cpp/)
**21st – 23rd September**
1. **C++ Fundamentals You Wish You Had Known Earlier** – Mateusz Pusz – 3 day online workshop available on 21st– 23rd September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-cpp-fundamentals/](https://cppcon.org/class-2026-cpp-fundamentals/)
2. **C++23 in Practice: A Complete Introduction** – Nicolai Josuttis – 3 day online workshop available on 21st– 23rd September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-cpp23-in-practice/](https://cppcon.org/class-2026-cpp23-in-practice/)
3. **Programming with C++20** – Andreas Fertig – 3 day online workshop available on 21st– 23rd September 09.00 – 15.00 MDT – [https://cppcon.org/class-2026-programming-with-cpp20/](https://cppcon.org/class-2026-programming-with-cpp20/)
**26th – 27th September**
1. **Using C++ for Low-Latency Systems** – Patrice Roy – 2 day online workshop available on 26th– 27th September 09.00 – 17.00 MDT – [https://cppcon.org/class-2026-low-latency/](https://cppcon.org/class-2026-low-latency/)
This is the latest news from upcoming C++ Conferences. You can review all of the news at [https://programmingarchive.com/upcoming-conference-news/](https://programmingarchive.com/upcoming-conference-news/)
**CppCon Onsite Workshops**
All onsite workshops will take place in the Gaylord Rockies in Aurora, Colorado
**12th & 13th September**
1. **Advanced and Modern C++ Programming: The Tricky Parts** – Nicolai Josuttis – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-tricky-parts/](https://cppcon.org/class-2026-tricky-parts/)
2. **C++ Best Practices** – Jason Turner – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-best-practices/](https://cppcon.org/class-2026-best-practices/)
3. **How Hardware Gets Hacked: Breaking and Defending Embedded Systems** – Nathan Jones – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-hardware-hack/](https://cppcon.org/class-2026-hardware-hack/)
4. **Mastering \`std::execution\`: A Hands-On Workshop** – Mateusz Pusz – 2 day in-person
workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-execution/](https://cppcon.org/class-2026-execution/)
5. **Performance and Efficiency in C++ for Experts, Future Experts, and Everyone Else** – Fedor Pikus – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-performance-and-efficiency/](https://cppcon.org/class-2026-performance-and-efficiency/)
6. **Talking Tech** – Sherry Sontag – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-talking-tech/](https://cppcon.org/class-2026-talking-tech/)
**13th September**
1. **AI++ 101 : Build a C++ Coding Agent from Scratch** – Jody Hagins – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-AI101/](https://cppcon.org/class-2026-AI101/)
2. **Essential GDB and Linux System Tools** – Mike Shah – 1 day in-person workshop available on 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-essential-gdb/](https://cppcon.org/class-2026-essential-gdb/)
**19th & 20th September**
1. **AI++ 201: Building High Quality C++ Infrastructure with AI** – Jody Hagins – 2 day in-person workshop available on 19th & 20th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-ai201/](https://cppcon.org/class-2026-ai201/)
2. **Function and Class Design with C++2x** – Jeff Garland – 2 day in-person workshop available on 19th & 20th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-function-class-design/](https://cppcon.org/class-2026-function-class-design/)
3. **High-performance Concurrency in C++** – Fedor Pikus – 2 day in-person workshop available on 19th & 20th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-high-perf-concurrency/](https://cppcon.org/class-2026-high-perf-concurrency/)
**OTHER NEWS**
* **Dates for ACCU on Sea 2027 Announced –** ACCU on Sea 2027 will take place in Folkestone from June 30th – July 3rd with pre-conference workshops taking place from June 28th – 29th
* **Boost Documentary screening at CppCon 2026** – Boost Libraries have announced that they will be screening a documentary on the history of Boost at CppCon 2026. Watch the trailer here [https://www.youtube.com/watch?v=87jvuDbnwqQ](https://www.youtube.com/watch?v=87jvuDbnwqQ)
* **C++Now 2026 Videos Now Being Released on YouTube** – Subscribe to the C++Now YouTube channel to stay up to date when each video is published – [https://www.youtube.com/@CppNow](https://www.youtube.com/@CppNow)
https://redd.it/1v93gvk
@r_cpp
5. **Performance and Efficiency in C++ for Experts, Future Experts, and Everyone Else** – Fedor Pikus – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-performance-and-efficiency/](https://cppcon.org/class-2026-performance-and-efficiency/)
6. **Talking Tech** – Sherry Sontag – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-talking-tech/](https://cppcon.org/class-2026-talking-tech/)
**13th September**
1. **AI++ 101 : Build a C++ Coding Agent from Scratch** – Jody Hagins – 2 day in-person workshop available on 12th & 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-AI101/](https://cppcon.org/class-2026-AI101/)
2. **Essential GDB and Linux System Tools** – Mike Shah – 1 day in-person workshop available on 13th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-essential-gdb/](https://cppcon.org/class-2026-essential-gdb/)
**19th & 20th September**
1. **AI++ 201: Building High Quality C++ Infrastructure with AI** – Jody Hagins – 2 day in-person workshop available on 19th & 20th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-ai201/](https://cppcon.org/class-2026-ai201/)
2. **Function and Class Design with C++2x** – Jeff Garland – 2 day in-person workshop available on 19th & 20th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-function-class-design/](https://cppcon.org/class-2026-function-class-design/)
3. **High-performance Concurrency in C++** – Fedor Pikus – 2 day in-person workshop available on 19th & 20th September – 09:00 – 17:00 – [https://cppcon.org/class-2026-high-perf-concurrency/](https://cppcon.org/class-2026-high-perf-concurrency/)
**OTHER NEWS**
* **Dates for ACCU on Sea 2027 Announced –** ACCU on Sea 2027 will take place in Folkestone from June 30th – July 3rd with pre-conference workshops taking place from June 28th – 29th
* **Boost Documentary screening at CppCon 2026** – Boost Libraries have announced that they will be screening a documentary on the history of Boost at CppCon 2026. Watch the trailer here [https://www.youtube.com/watch?v=87jvuDbnwqQ](https://www.youtube.com/watch?v=87jvuDbnwqQ)
* **C++Now 2026 Videos Now Being Released on YouTube** – Subscribe to the C++Now YouTube channel to stay up to date when each video is published – [https://www.youtube.com/@CppNow](https://www.youtube.com/@CppNow)
https://redd.it/1v93gvk
@r_cpp
amask = f(amask) on NEON. faster than the obvious blend
## Problem
Apply an operation to elements that satisfy a condition:
for (sizet i = 0; i < n; ++i)
if (mask(a[i])) a[i] = f(a[i]);
### Notes
- `a[i] ∈ (0, 1)`, `thd ∈ (0, 1)`, `mask = a[i] < thd`; uniform distribution (except at the end of the article)
- `f` is one of `sqrt`, `frfrexp` (mantissa), `sin` 3.5 ULP, `sin` 1 ULP, `pow` 1 ULP (from SLEEF)
- `f` and `mask` are passed as runtime values, so they are wrapped in a lambda with `alwaysinline
- Don't compare numbers across tables. Different conditions, values fluctuate
- All benchmarks: Apple M5; clang++ -O3 -std=c++23 -march=native; GiB/s = (n 4 bytes) / time, min of 720 runs
(During bench, functions run in a changing order, data is restored ofc); n=1e7 + 2432;
## BSL blend
If the problem is memory bound (cheap function or high density), the standard algorithm is optimal:
template <bool Skip>
void bsl(float dst, const size_t n, auto f, auto mask) {
for (size_t i = 0; i < n; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + i + 4 j);
std::array<uint32x4t, 4> m;
for (sizet j = 0; j < 4; ++j) mj = mask(vj);
if constexpr (Skip) {
if (vmaxvqu32(vaddqu32(vaddqu32(m[0], m[1]), vaddqu32(m2, m3))) == 0) continue;
}
for (sizet j = 0; j < 4; ++j) vst1qf32(dst + i + 4 j, vbslq_f32(m[j], f(v[j]), v[j]));
}
}
It computes `f` on every element, but stores only the selected ones.
Skip helps on sparse masks, but otherwise mispredictions will kill performance. We'll need it later.
But for expensive `f` this algo does too much extra work
## Detour
To avoid unnecessary work, we compress selected elements, apply only to them, and expand back.
avx512 does this in two instructions. NEON doesn't, so we'll emulate and optimize.
constexpr size_t tile = 4096;
constexpr std::array<uint32_t, 4> weights{1 + 16, 2 + 16, 4 + 16, 8 + 16};
constexpr auto cps_tbl = compress_table();
constexpr auto exp_tbl = expand_table();
std::array<float, tile + 16> tmp;
std::array<uint8_t, tile / 4 + 3> s;
std::array<uint16_t, tile / 4 + 3> idx; // idx, D and B come in later
constexpr double D = 0.845;
constexpr double B = 0.3;
template <bool Skip>
size_t detour(float dst, const sizet n, const auto w, auto f, auto mask) {
float* ptr = tmp.data();
for (sizet i = 0; i < n; i += 16) {
std::array<float32x4t, 4> v;
for (sizet j = 0; j < 4; ++j) vj = vld1qf32(dst + i + 4 * j);
std::array<uint32x4t, 4> m;
for (sizet j = 0; j < 4; ++j) m[j] = mask(v[j]);
if constexpr (Skip)
if (vmaxvqu32(vaddqu32(vaddqu32(m0, m1), vaddqu32(m[2], m[3]))) == 0) {
s[i / 4] = s[i / 4 + 1] = s[i / 4 + 2] = s[i / 4 + 3] = 0;
continue;
}
std::array<uint32t, 4> sk;
for (sizet j = 0; j < 4; ++j) {
sk[j] = vaddvqu32(vandqu32(m[j], w));
s[i / 4 + j] = sk[j];
}
std::array<sizet, 4> off; off0 = 0;
for (sizet j = 1; j < 4; ++j) off[j] = off[j - 1] + (sk[j - 1] >> 4);
std::array<uint8x16t, 4> index;
for (sizet j = 0; j < 4; ++j) index[j] = vld1qu8(cpstbl[sk[j] & 15].data());
for (sizet j = 0; j < 4; ++j) vst1qf32(ptr + off[j], vreinterpretqf32u8(vqtbl1qu8(vreinterpretqu8f32(vj), indexj)));
ptr += off3 + (sk3 >> 4);
}
const sizet size = ptr - tmp.data();
if (size == 0) return size;
ptr = tmp.data();
for (sizet i = 0; i < size; i += 16) {
## Problem
Apply an operation to elements that satisfy a condition:
for (sizet i = 0; i < n; ++i)
if (mask(a[i])) a[i] = f(a[i]);
### Notes
- `a[i] ∈ (0, 1)`, `thd ∈ (0, 1)`, `mask = a[i] < thd`; uniform distribution (except at the end of the article)
- `f` is one of `sqrt`, `frfrexp` (mantissa), `sin` 3.5 ULP, `sin` 1 ULP, `pow` 1 ULP (from SLEEF)
- `f` and `mask` are passed as runtime values, so they are wrapped in a lambda with `alwaysinline
, otherwise they may
not be inlined
- The array size n is a multiple of every unroll, tile etc. The tail is trivial to handle(BSL/scalar)
- In tables ` = best, units = GiB/s- Don't compare numbers across tables. Different conditions, values fluctuate
- All benchmarks: Apple M5; clang++ -O3 -std=c++23 -march=native; GiB/s = (n 4 bytes) / time, min of 720 runs
(During bench, functions run in a changing order, data is restored ofc); n=1e7 + 2432;
## BSL blend
If the problem is memory bound (cheap function or high density), the standard algorithm is optimal:
template <bool Skip>
void bsl(float dst, const size_t n, auto f, auto mask) {
for (size_t i = 0; i < n; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + i + 4 j);
std::array<uint32x4t, 4> m;
for (sizet j = 0; j < 4; ++j) mj = mask(vj);
if constexpr (Skip) {
if (vmaxvqu32(vaddqu32(vaddqu32(m[0], m[1]), vaddqu32(m2, m3))) == 0) continue;
}
for (sizet j = 0; j < 4; ++j) vst1qf32(dst + i + 4 j, vbslq_f32(m[j], f(v[j]), v[j]));
}
}
It computes `f` on every element, but stores only the selected ones.
Skip helps on sparse masks, but otherwise mispredictions will kill performance. We'll need it later.
But for expensive `f` this algo does too much extra work
## Detour
To avoid unnecessary work, we compress selected elements, apply only to them, and expand back.
avx512 does this in two instructions. NEON doesn't, so we'll emulate and optimize.
constexpr size_t tile = 4096;
constexpr std::array<uint32_t, 4> weights{1 + 16, 2 + 16, 4 + 16, 8 + 16};
constexpr auto cps_tbl = compress_table();
constexpr auto exp_tbl = expand_table();
std::array<float, tile + 16> tmp;
std::array<uint8_t, tile / 4 + 3> s;
std::array<uint16_t, tile / 4 + 3> idx; // idx, D and B come in later
constexpr double D = 0.845;
constexpr double B = 0.3;
template <bool Skip>
size_t detour(float dst, const sizet n, const auto w, auto f, auto mask) {
float* ptr = tmp.data();
for (sizet i = 0; i < n; i += 16) {
std::array<float32x4t, 4> v;
for (sizet j = 0; j < 4; ++j) vj = vld1qf32(dst + i + 4 * j);
std::array<uint32x4t, 4> m;
for (sizet j = 0; j < 4; ++j) m[j] = mask(v[j]);
if constexpr (Skip)
if (vmaxvqu32(vaddqu32(vaddqu32(m0, m1), vaddqu32(m[2], m[3]))) == 0) {
s[i / 4] = s[i / 4 + 1] = s[i / 4 + 2] = s[i / 4 + 3] = 0;
continue;
}
std::array<uint32t, 4> sk;
for (sizet j = 0; j < 4; ++j) {
sk[j] = vaddvqu32(vandqu32(m[j], w));
s[i / 4 + j] = sk[j];
}
std::array<sizet, 4> off; off0 = 0;
for (sizet j = 1; j < 4; ++j) off[j] = off[j - 1] + (sk[j - 1] >> 4);
std::array<uint8x16t, 4> index;
for (sizet j = 0; j < 4; ++j) index[j] = vld1qu8(cpstbl[sk[j] & 15].data());
for (sizet j = 0; j < 4; ++j) vst1qf32(ptr + off[j], vreinterpretqf32u8(vqtbl1qu8(vreinterpretqu8f32(vj), indexj)));
ptr += off3 + (sk3 >> 4);
}
const sizet size = ptr - tmp.data();
if (size == 0) return size;
ptr = tmp.data();
for (sizet i = 0; i < size; i += 16) {
a[mask] = f(a[mask]) on NEON. faster than the obvious blend
## Problem
Apply an operation to elements that satisfy a condition:
for (size_t i = 0; i < n; ++i)
if (mask(a[i])) a[i] = f(a[i]);
### Notes
- `a[i] ∈ (0, 1)`, `thd ∈ (0, 1)`, `mask = a[i] < thd`; uniform distribution (except at the end of the article)
- `f` is one of `sqrt`, `frfrexp` (mantissa), `sin` 3.5 ULP, `sin` 1 ULP, `pow` 1 ULP (from SLEEF)
- `f` and `mask` are passed as runtime values, so they are wrapped in a lambda with `always_inline`, otherwise they may
not be inlined
- The array size `n` is a multiple of every unroll, tile etc. The tail is trivial to handle(BSL/scalar)
- In tables `*` = best, units = GiB/s
- Don't compare numbers across tables. Different conditions, values fluctuate
- All benchmarks: Apple M5; clang++ -O3 -std=c++23 -march=native; GiB/s = (n * 4 bytes) / time, min of 720 runs
(During bench, functions run in a changing order, data is restored ofc); n=1e7 + 2432;
## BSL blend
If the problem is memory bound (cheap function or high density), the standard algorithm is optimal:
template <bool Skip>
void bsl(float* dst, const size_t n, auto f, auto mask) {
for (size_t i = 0; i < n; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + i + 4 * j);
std::array<uint32x4_t, 4> m;
for (size_t j = 0; j < 4; ++j) m[j] = mask(v[j]);
if constexpr (Skip) {
if (vmaxvq_u32(vaddq_u32(vaddq_u32(m[0], m[1]), vaddq_u32(m[2], m[3]))) == 0) continue;
}
for (size_t j = 0; j < 4; ++j) vst1q_f32(dst + i + 4 * j, vbslq_f32(m[j], f(v[j]), v[j]));
}
}
It computes `f` on every element, but stores only the selected ones.
Skip helps on sparse masks, but otherwise mispredictions will kill performance. We'll need it later.
But for expensive `f` this algo does too much extra work
## Detour
To avoid unnecessary work, we compress selected elements, apply only to them, and expand back.
avx512 does this in two instructions. NEON doesn't, so we'll emulate and optimize.
constexpr size_t tile = 4096;
constexpr std::array<uint32_t, 4> weights{1 + 16, 2 + 16, 4 + 16, 8 + 16};
constexpr auto cps_tbl = compress_table();
constexpr auto exp_tbl = expand_table();
std::array<float, tile + 16> tmp;
std::array<uint8_t, tile / 4 + 3> s;
std::array<uint16_t, tile / 4 + 3> idx; // idx, D and B come in later
constexpr double D = 0.845;
constexpr double B = 0.3;
template <bool Skip>
size_t detour(float* dst, const size_t n, const auto w, auto f, auto mask) {
float* ptr = tmp.data();
for (size_t i = 0; i < n; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + i + 4 * j);
std::array<uint32x4_t, 4> m;
for (size_t j = 0; j < 4; ++j) m[j] = mask(v[j]);
if constexpr (Skip)
if (vmaxvq_u32(vaddq_u32(vaddq_u32(m[0], m[1]), vaddq_u32(m[2], m[3]))) == 0) {
s[i / 4] = s[i / 4 + 1] = s[i / 4 + 2] = s[i / 4 + 3] = 0;
continue;
}
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) {
sk[j] = vaddvq_u32(vandq_u32(m[j], w));
s[i / 4 + j] = sk[j];
}
std::array<size_t, 4> off; off[0] = 0;
for (size_t j = 1; j < 4; ++j) off[j] = off[j - 1] + (sk[j - 1] >> 4);
std::array<uint8x16_t, 4> index;
for (size_t j = 0; j < 4; ++j) index[j] = vld1q_u8(cps_tbl[sk[j] & 15].data());
for (size_t j = 0; j < 4; ++j) vst1q_f32(ptr + off[j], vreinterpretq_f32_u8(vqtbl1q_u8(vreinterpretq_u8_f32(v[j]), index[j])));
ptr += off[3] + (sk[3] >> 4);
}
const size_t size = ptr - tmp.data();
if (size == 0) return size;
ptr = tmp.data();
for (size_t i = 0; i < size; i += 16) {
## Problem
Apply an operation to elements that satisfy a condition:
for (size_t i = 0; i < n; ++i)
if (mask(a[i])) a[i] = f(a[i]);
### Notes
- `a[i] ∈ (0, 1)`, `thd ∈ (0, 1)`, `mask = a[i] < thd`; uniform distribution (except at the end of the article)
- `f` is one of `sqrt`, `frfrexp` (mantissa), `sin` 3.5 ULP, `sin` 1 ULP, `pow` 1 ULP (from SLEEF)
- `f` and `mask` are passed as runtime values, so they are wrapped in a lambda with `always_inline`, otherwise they may
not be inlined
- The array size `n` is a multiple of every unroll, tile etc. The tail is trivial to handle(BSL/scalar)
- In tables `*` = best, units = GiB/s
- Don't compare numbers across tables. Different conditions, values fluctuate
- All benchmarks: Apple M5; clang++ -O3 -std=c++23 -march=native; GiB/s = (n * 4 bytes) / time, min of 720 runs
(During bench, functions run in a changing order, data is restored ofc); n=1e7 + 2432;
## BSL blend
If the problem is memory bound (cheap function or high density), the standard algorithm is optimal:
template <bool Skip>
void bsl(float* dst, const size_t n, auto f, auto mask) {
for (size_t i = 0; i < n; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + i + 4 * j);
std::array<uint32x4_t, 4> m;
for (size_t j = 0; j < 4; ++j) m[j] = mask(v[j]);
if constexpr (Skip) {
if (vmaxvq_u32(vaddq_u32(vaddq_u32(m[0], m[1]), vaddq_u32(m[2], m[3]))) == 0) continue;
}
for (size_t j = 0; j < 4; ++j) vst1q_f32(dst + i + 4 * j, vbslq_f32(m[j], f(v[j]), v[j]));
}
}
It computes `f` on every element, but stores only the selected ones.
Skip helps on sparse masks, but otherwise mispredictions will kill performance. We'll need it later.
But for expensive `f` this algo does too much extra work
## Detour
To avoid unnecessary work, we compress selected elements, apply only to them, and expand back.
avx512 does this in two instructions. NEON doesn't, so we'll emulate and optimize.
constexpr size_t tile = 4096;
constexpr std::array<uint32_t, 4> weights{1 + 16, 2 + 16, 4 + 16, 8 + 16};
constexpr auto cps_tbl = compress_table();
constexpr auto exp_tbl = expand_table();
std::array<float, tile + 16> tmp;
std::array<uint8_t, tile / 4 + 3> s;
std::array<uint16_t, tile / 4 + 3> idx; // idx, D and B come in later
constexpr double D = 0.845;
constexpr double B = 0.3;
template <bool Skip>
size_t detour(float* dst, const size_t n, const auto w, auto f, auto mask) {
float* ptr = tmp.data();
for (size_t i = 0; i < n; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + i + 4 * j);
std::array<uint32x4_t, 4> m;
for (size_t j = 0; j < 4; ++j) m[j] = mask(v[j]);
if constexpr (Skip)
if (vmaxvq_u32(vaddq_u32(vaddq_u32(m[0], m[1]), vaddq_u32(m[2], m[3]))) == 0) {
s[i / 4] = s[i / 4 + 1] = s[i / 4 + 2] = s[i / 4 + 3] = 0;
continue;
}
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) {
sk[j] = vaddvq_u32(vandq_u32(m[j], w));
s[i / 4 + j] = sk[j];
}
std::array<size_t, 4> off; off[0] = 0;
for (size_t j = 1; j < 4; ++j) off[j] = off[j - 1] + (sk[j - 1] >> 4);
std::array<uint8x16_t, 4> index;
for (size_t j = 0; j < 4; ++j) index[j] = vld1q_u8(cps_tbl[sk[j] & 15].data());
for (size_t j = 0; j < 4; ++j) vst1q_f32(ptr + off[j], vreinterpretq_f32_u8(vqtbl1q_u8(vreinterpretq_u8_f32(v[j]), index[j])));
ptr += off[3] + (sk[3] >> 4);
}
const size_t size = ptr - tmp.data();
if (size == 0) return size;
ptr = tmp.data();
for (size_t i = 0; i < size; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(ptr + i + 4 * j);
for (size_t j = 0; j < 4; ++j) vst1q_f32(ptr + i + 4 * j, f(v[j]));
}
for (size_t i = 0; i < n; i += 16) {
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) sk[j] = s[i / 4 + j];
std::array<size_t, 4> off{};
for (size_t j = 1; j < 4; ++j) off[j] = off[j - 1] + (sk[j - 1] >> 4);
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(ptr + off[j]);
std::array<float32x4_t, 4> a;
for (size_t j = 0; j < 4; ++j) a[j] = vld1q_f32(dst + i + 4 * j);
std::array<uint8x16_t, 4> index;
for (size_t j = 0; j < 4; ++j) index[j] = vld1q_u8(exp_tbl[sk[j] & 15].data());
std::array<uint8x16x2_t, 4> tbl;
for (size_t j = 0; j < 4; ++j) tbl[j] = {{vreinterpretq_u8_f32(v[j]), vreinterpretq_u8_f32(a[j])}};
for (size_t j = 0; j < 4; ++j) vst1q_f32(dst + i + 4 * j, vreinterpretq_f32_u8(vqtbl2q_u8(tbl[j], index[j])));
ptr += off[3] + (sk[3] >> 4);
}
return size;
}
void tiled_detour(float* dst, const size_t n, auto f, auto mask) {
const auto w = vld1q_u32(weights.data());
for (size_t i = 0; i < n; i += tile)
detour<false>(dst + i, tile, w, f, mask);
}
compress is the same as
in [my previous post](https://www.reddit.com/r/cpp/comments/1uqscnr/stream_compaction_on_neon_vectorizing_copy_if_by/).
expand_table: for true lanes it selects the next element from the compressed register (bytes from `[0, 15]`),
and for false lanes, selects the same bytes + 16.
Then tbl2 on {processed, original}, the same trick as in compress basically
Also:
- expand is skipped for free on empty tiles
- `s` is saved for free to avoid recomputing addv
- cache-sized tiling.
- Empirically `tile=4096` is optimal.
BSL speed doesn't depend on density: `sqrt 32.1`, `sin35 - 9.7`, `pow10 1.02`.
| thd | 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 |
|:-------------|--------:|--------:|--------:|--------:|--------:|--------:|-------:|-------:|-------:|-------:|------:|
| detour sqrt | 38.03\* | 18.81 | 17.78 | 16.82 | 15.98 | 15.21 | 14.51 | 13.85 | 13.24 | 12.71 | 12.30 |
| detour sin35 | 38.16\* | 16.70\* | 14.46\* | 12.71\* | 11.36\* | 10.25\* | 9.35 | 8.59 | 7.95 | 7.39 | 6.95 |
| detour pow10 | 38.09\* | 6.83\* | 4.13\* | 2.96\* | 2.31\* | 1.89\* | 1.60\* | 1.39\* | 1.22\* | 1.10\* | 0.99 |
For cheap sqrt BSL is always better (except thd = 0, obviously).
And for very expensive pow, detour is better (except thd = 1, of course).
## When detour wins
Define:
- `B` = BSL(`vld + vbsl + vst`) overhead per register.
- `D` = detour(compress + expand) overhead per register.
- `T` = cost of `f` per register (we assume `cost of f >> cost of mask`, affects only calibration accuracy).
- `d` = fraction of selected elements
BSL applies `f` to every register. detour applies it only to `d` of them, so it saves `T * (1 - d)`.
Detour wins when the saving outweighs `D - B`.
`T * (1 - d) > D - B`
So detour pays off when `d < d_max = 1 - (D - B) / T`.
`B` and `D` depend only on hardware, so let's premeasure them (I have `B = 0.3`, `D = 0.845` ns/register)
`T` we measure over the first few tiles, timing BSL. And from it, we also find `d_max`:
template<size_t tile>
double bsl_calibrate(float* dst, const size_t len, auto f, auto mask) {
double ns = 1e18;
for (size_t i = 0; i < len; i += tile) {
const auto st = std::chrono::high_resolution_clock::now();
bsl<false>(dst, tile, f, mask);
const auto ed = std::chrono::high_resolution_clock::now();
ns = std::min(ns, std::chrono::duration<double, std::nano>(ed - st).count());
dst += tile;
}
const double t = std::max(1e-9, ns / (tile / 4.0) - B);
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(ptr + i + 4 * j);
for (size_t j = 0; j < 4; ++j) vst1q_f32(ptr + i + 4 * j, f(v[j]));
}
for (size_t i = 0; i < n; i += 16) {
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) sk[j] = s[i / 4 + j];
std::array<size_t, 4> off{};
for (size_t j = 1; j < 4; ++j) off[j] = off[j - 1] + (sk[j - 1] >> 4);
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(ptr + off[j]);
std::array<float32x4_t, 4> a;
for (size_t j = 0; j < 4; ++j) a[j] = vld1q_f32(dst + i + 4 * j);
std::array<uint8x16_t, 4> index;
for (size_t j = 0; j < 4; ++j) index[j] = vld1q_u8(exp_tbl[sk[j] & 15].data());
std::array<uint8x16x2_t, 4> tbl;
for (size_t j = 0; j < 4; ++j) tbl[j] = {{vreinterpretq_u8_f32(v[j]), vreinterpretq_u8_f32(a[j])}};
for (size_t j = 0; j < 4; ++j) vst1q_f32(dst + i + 4 * j, vreinterpretq_f32_u8(vqtbl2q_u8(tbl[j], index[j])));
ptr += off[3] + (sk[3] >> 4);
}
return size;
}
void tiled_detour(float* dst, const size_t n, auto f, auto mask) {
const auto w = vld1q_u32(weights.data());
for (size_t i = 0; i < n; i += tile)
detour<false>(dst + i, tile, w, f, mask);
}
compress is the same as
in [my previous post](https://www.reddit.com/r/cpp/comments/1uqscnr/stream_compaction_on_neon_vectorizing_copy_if_by/).
expand_table: for true lanes it selects the next element from the compressed register (bytes from `[0, 15]`),
and for false lanes, selects the same bytes + 16.
Then tbl2 on {processed, original}, the same trick as in compress basically
Also:
- expand is skipped for free on empty tiles
- `s` is saved for free to avoid recomputing addv
- cache-sized tiling.
- Empirically `tile=4096` is optimal.
BSL speed doesn't depend on density: `sqrt 32.1`, `sin35 - 9.7`, `pow10 1.02`.
| thd | 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 |
|:-------------|--------:|--------:|--------:|--------:|--------:|--------:|-------:|-------:|-------:|-------:|------:|
| detour sqrt | 38.03\* | 18.81 | 17.78 | 16.82 | 15.98 | 15.21 | 14.51 | 13.85 | 13.24 | 12.71 | 12.30 |
| detour sin35 | 38.16\* | 16.70\* | 14.46\* | 12.71\* | 11.36\* | 10.25\* | 9.35 | 8.59 | 7.95 | 7.39 | 6.95 |
| detour pow10 | 38.09\* | 6.83\* | 4.13\* | 2.96\* | 2.31\* | 1.89\* | 1.60\* | 1.39\* | 1.22\* | 1.10\* | 0.99 |
For cheap sqrt BSL is always better (except thd = 0, obviously).
And for very expensive pow, detour is better (except thd = 1, of course).
## When detour wins
Define:
- `B` = BSL(`vld + vbsl + vst`) overhead per register.
- `D` = detour(compress + expand) overhead per register.
- `T` = cost of `f` per register (we assume `cost of f >> cost of mask`, affects only calibration accuracy).
- `d` = fraction of selected elements
BSL applies `f` to every register. detour applies it only to `d` of them, so it saves `T * (1 - d)`.
Detour wins when the saving outweighs `D - B`.
`T * (1 - d) > D - B`
So detour pays off when `d < d_max = 1 - (D - B) / T`.
`B` and `D` depend only on hardware, so let's premeasure them (I have `B = 0.3`, `D = 0.845` ns/register)
`T` we measure over the first few tiles, timing BSL. And from it, we also find `d_max`:
template<size_t tile>
double bsl_calibrate(float* dst, const size_t len, auto f, auto mask) {
double ns = 1e18;
for (size_t i = 0; i < len; i += tile) {
const auto st = std::chrono::high_resolution_clock::now();
bsl<false>(dst, tile, f, mask);
const auto ed = std::chrono::high_resolution_clock::now();
ns = std::min(ns, std::chrono::duration<double, std::nano>(ed - st).count());
dst += tile;
}
const double t = std::max(1e-9, ns / (tile / 4.0) - B);
Reddit
From the cpp community on Reddit: Stream compaction on NEON: vectorizing copy_if by hand (30x)
Explore this post and more from the cpp community
return 1.0 - (D - B) / t;
}
Here:
- empirically 4 tiles of 2048 are enough
- min over measurements is less noisy than mean
- We'll run the first few tiles with calibration
- measure bsl, because it always computes `f`, so `T = t_bsl - B`
- std::max here protects against divide-by-zero and against `t < 0` when `T` is very cheap
### pilot v1
When `d_max < 0` bsl is always faster:
void pilot_v1(float* dst, const size_t n, auto f, auto mask) {
const auto w = vld1q_u32(weights.data());
const float d_max = bsl_calibrate<tile / 2>(dst, 2 * tile, f, mask);
dst += 2 * tile;
for (size_t i = 2 * tile; i < n; i += tile) {
if (d_max < 0)
bsl<false>(dst, tile, f, mask);
else
detour<false>(dst, tile, w, f, mask);
dst += tile;
}
}
| thd | tiled detour sqrt | pilot v1 sqrt | BSL sqrt | tiled detour sin35 | pilot v1 sin35 | BSL sin35 |
|----:|------------------:|--------------:|---------:|-------------------:|---------------:|----------:|
| 0 | 38.37\* | 32.19 | 32.24 | 38.26\* | 38.16 | 9.75 |
| 0.3 | 16.89 | 32.18 | 32.24\* | 12.72\* | 12.71 | 9.75 |
| 0.6 | 14.54 | 32.18\* | 32.18\* | 9.25 | 9.36 | 9.75\* |
| 1 | 12.29 | 32.21 | 32.25\* | 6.87 | 6.86 | 9.75\* |
The algorithm got sqrt right.
But for sin35 at high thd, detour is selected, and we lose 30%:
v1 switches to bsl only when it's faster at every density.
### pilot v2
It's expensive to calculate the density of the whole tile, so we'll use the first 256
(It reads 6% of the tile, which is noise on pow, but noticeable on sqrt)
For a more or less uniform distribution this is enough:
size_t density(float* dst, const size_t n, auto mask) {
std::array<uint32x4_t, 4> acc;
acc.fill(vdupq_n_u32(0));
for (size_t i = 0; i < n; i += 16) {
for (size_t j = 0; j < 4; ++j)
acc[j] = vsubq_u32(acc[j], mask(vld1q_f32(dst + i + 4 * j)));
}
return vaddvq_u32(vaddq_u32(vaddq_u32(acc[0], acc[1]), vaddq_u32(acc[2], acc[3])));
}
Trick here: true lane of bitmask = 0xFFFFFFFF = -1, subtracting the lane actually adds.
Skip wins when the predictor rarely mispredicts, i.e. an 80% chance that all 4 registers are empty.
The density is approximately `0.014` (`(1 - x)^16 = 0.8`)
void pilot_v2(float* dst, const size_t n, auto f, auto mask) {
const auto w = vld1q_u32(weights.data());
const float d_max = bsl_calibrate<tile / 2>(dst, 2 * tile, f, mask);
constexpr size_t probe = 256;
constexpr size_t xlo = 0.014 * probe;
const long long hi = d_max * probe;
dst += 2 * tile;
for (size_t i = 2 * tile; i < n; i += tile) {
const long long cnt = density(dst, probe, mask);
if (cnt > hi) {
if (cnt < xlo)
bsl<true>(dst, tile, f, mask);
else
bsl<false>(dst, tile, f, mask);
} else if (cnt < xlo)
detour<true>(dst, tile, w, f, mask);
else
detour<false>(dst, tile, w, f, mask);
dst += tile;
}
}
`hi` and `xlo` here are `d_max` and `0.014` cutoffs, but multiplied by the probe length (256).
| thd | 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 |
|:---------------|--------:|--------:|--------:|--------:|--------:|--------:|-------:|-------:|-------:|-------:|-------:|
| pilot_v1 sin35 | 38.25 | 16.72\* | 14.48\* | 12.77\* | 11.41\* | 10.33\* | 9.40 | 8.62 | 7.93 | 7.36 | 6.91 |
| pilot_v2 sin35 | 77.29\* | 16.55 | 14.34 | 12.65 | 11.33 | 10.14 | 9.71 | 9.73 | 9.74 | 9.73 | 9.73 |
| BSL sin35 | 9.78 | 9.77 | 9.77 | 9.76 | 9.76 | 9.78 | 9.78\* | 9.77\* | 9.78\* | 9.77\* | 9.78\* |
|
}
Here:
- empirically 4 tiles of 2048 are enough
- min over measurements is less noisy than mean
- We'll run the first few tiles with calibration
- measure bsl, because it always computes `f`, so `T = t_bsl - B`
- std::max here protects against divide-by-zero and against `t < 0` when `T` is very cheap
### pilot v1
When `d_max < 0` bsl is always faster:
void pilot_v1(float* dst, const size_t n, auto f, auto mask) {
const auto w = vld1q_u32(weights.data());
const float d_max = bsl_calibrate<tile / 2>(dst, 2 * tile, f, mask);
dst += 2 * tile;
for (size_t i = 2 * tile; i < n; i += tile) {
if (d_max < 0)
bsl<false>(dst, tile, f, mask);
else
detour<false>(dst, tile, w, f, mask);
dst += tile;
}
}
| thd | tiled detour sqrt | pilot v1 sqrt | BSL sqrt | tiled detour sin35 | pilot v1 sin35 | BSL sin35 |
|----:|------------------:|--------------:|---------:|-------------------:|---------------:|----------:|
| 0 | 38.37\* | 32.19 | 32.24 | 38.26\* | 38.16 | 9.75 |
| 0.3 | 16.89 | 32.18 | 32.24\* | 12.72\* | 12.71 | 9.75 |
| 0.6 | 14.54 | 32.18\* | 32.18\* | 9.25 | 9.36 | 9.75\* |
| 1 | 12.29 | 32.21 | 32.25\* | 6.87 | 6.86 | 9.75\* |
The algorithm got sqrt right.
But for sin35 at high thd, detour is selected, and we lose 30%:
v1 switches to bsl only when it's faster at every density.
### pilot v2
It's expensive to calculate the density of the whole tile, so we'll use the first 256
(It reads 6% of the tile, which is noise on pow, but noticeable on sqrt)
For a more or less uniform distribution this is enough:
size_t density(float* dst, const size_t n, auto mask) {
std::array<uint32x4_t, 4> acc;
acc.fill(vdupq_n_u32(0));
for (size_t i = 0; i < n; i += 16) {
for (size_t j = 0; j < 4; ++j)
acc[j] = vsubq_u32(acc[j], mask(vld1q_f32(dst + i + 4 * j)));
}
return vaddvq_u32(vaddq_u32(vaddq_u32(acc[0], acc[1]), vaddq_u32(acc[2], acc[3])));
}
Trick here: true lane of bitmask = 0xFFFFFFFF = -1, subtracting the lane actually adds.
Skip wins when the predictor rarely mispredicts, i.e. an 80% chance that all 4 registers are empty.
The density is approximately `0.014` (`(1 - x)^16 = 0.8`)
void pilot_v2(float* dst, const size_t n, auto f, auto mask) {
const auto w = vld1q_u32(weights.data());
const float d_max = bsl_calibrate<tile / 2>(dst, 2 * tile, f, mask);
constexpr size_t probe = 256;
constexpr size_t xlo = 0.014 * probe;
const long long hi = d_max * probe;
dst += 2 * tile;
for (size_t i = 2 * tile; i < n; i += tile) {
const long long cnt = density(dst, probe, mask);
if (cnt > hi) {
if (cnt < xlo)
bsl<true>(dst, tile, f, mask);
else
bsl<false>(dst, tile, f, mask);
} else if (cnt < xlo)
detour<true>(dst, tile, w, f, mask);
else
detour<false>(dst, tile, w, f, mask);
dst += tile;
}
}
`hi` and `xlo` here are `d_max` and `0.014` cutoffs, but multiplied by the probe length (256).
| thd | 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 |
|:---------------|--------:|--------:|--------:|--------:|--------:|--------:|-------:|-------:|-------:|-------:|-------:|
| pilot_v1 sin35 | 38.25 | 16.72\* | 14.48\* | 12.77\* | 11.41\* | 10.33\* | 9.40 | 8.62 | 7.93 | 7.36 | 6.91 |
| pilot_v2 sin35 | 77.29\* | 16.55 | 14.34 | 12.65 | 11.33 | 10.14 | 9.71 | 9.73 | 9.74 | 9.73 | 9.73 |
| BSL sin35 | 9.78 | 9.77 | 9.77 | 9.76 | 9.76 | 9.78 | 9.78\* | 9.77\* | 9.78\* | 9.77\* | 9.78\* |
|
pilot_v1 sin10 | 38.28 | 14.30\* | 11.18\* | 9.18\* | 7.75\* | 6.73\* | 5.94\* | 5.31\* | 4.78 | 4.36 | 4.02 |
| pilot_v2 sin10 | 76.60\* | 14.16 | 11.09 | 9.11 | 7.73 | 6.70 | 5.92 | 5.31\* | 4.82 | 4.84 | 4.84 |
| BSL sin10 | 4.85 | 4.85 | 4.84 | 4.85 | 4.84 | 4.84 | 4.85 | 4.85 | 4.85\* | 4.85\* | 4.85\* |
At thd = 0, Skip gives a huge win. For high thd, bsl is selected correctly.
But now on sparse masks, expand for empty registers is wasted.
### pilot v3
New detour version: during compress, we'll store only the indices of non-empty registers (into the `idx` buffer).
And expand will iterate over them:
template <bool Skip>
size_t detour_compact(float* dst, const size_t n, const auto w, auto f, auto mask) {
float* ptr = tmp.data();
size_t k = 0;
for (size_t i = 0; i < n; i += 16) {
// ... same as detour
if constexpr (Skip)
if (vmaxvq_u32(vaddq_u32(vaddq_u32(m[0], m[1]), vaddq_u32(m[2], m[3]))) == 0) continue;
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) sk[j] = vaddvq_u32(vandq_u32(m[j], w));
for (size_t j = 0; j < 4; ++j) {
s[k] = sk[j];
idx[k] = i + 4 * j;
k += bool(sk[j]);
}
// ... same as detour
}
// ... same as detour
for (size_t j = 0; j < 3; ++j) s[k + j] = 0, idx[k + j] = 0;
// ... same as detour
k = (k + 3) & ~size_t(3);
for (size_t i = 0; i < k; i += 4) {
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) sk[j] = s[i + j];
std::array<float32x4_t, 4> a;
for (size_t j = 0; j < 4; ++j) a[j] = vld1q_f32(dst + idx[i + j]);
// ... same as detour
for (size_t j = 0; j < 4; ++j)
vst1q_f32(dst + idx[i + j], vreinterpretq_f32_u8(vqtbl2q_u8(tbl[j], index[j])));
ptr += off[3] + (sk[3] >> 4);
}
return size;
}
k (number of non-empty registers) is rounded up to a multiple of 4 before expand,
so the unrolled loop has no tail left.
No branches in the hot loops: they'd kill speed, so compress runs on all four registers.
Instead of branches, the position of the current element is advanced by `bool(sk)`.
detour_compact wins when at least 50% of all registers are empty. The density is approximately 0.16 (`(1 - x) ^ 4 = 0.5`).
void pilot_v3(float* dst, const size_t n, auto f, auto mask) {
// ... same as v2
constexpr size_t lo = 0.16 * probe;
for (size_t i = 2 * tile; i < n; i += tile) {
const long long cnt = density(dst, probe, mask);
if (cnt > hi) {
if (cnt < xlo)
bsl<true>(dst, tile, f, mask);
else
bsl<false>(dst, tile, f, mask);
} else if (cnt < xlo)
detour_compact<true>(dst, tile, w, f, mask);
else if (cnt < lo)
detour_compact<false>(dst, tile, w, f, mask);
else
detour<false>(dst, tile, w, f, mask);
dst += tile;
}
}
And v3 is noticeably faster at low density:
| thd | 0 | 0.02 | 0.04 | 0.06 | 0.08 | 0.1 | 0.12 | 0.14 | 0.16 | 0.18 | 0.2 |
|:---------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|
| pilot_v2 sin35 | 77.29\* | 18.67 | 18.40 | 17.70 | 17.18 | 16.66 | 16.15 | 15.68 | 15.24\* | 14.82\* | 14.43\* |
| pilot_v3 sin35 | 77.26 | 23.71\* | 22.19\* | 20.48\* | 19.03\* | 17.82\* | 16.73\* | 15.76\* | 15.16 | 14.74 | 14.38 |
| pilot_v2 pow10 | 72.88 | 13.93 | 11.32 | 9.38 | 8.00 | 6.97 | 6.18 | 5.54 | 5.02\* | 4.59\* | 4.24\* |
| pilot_v3 pow10 | 73.71\* | 16.54\* | 12.63\* | 10.07\* | 8.39\* | 7.17\* | 6.26\* | 5.55\* | 5.02\* | 4.59\* | 4.24\* |
Now the algo is fast,
| pilot_v2 sin10 | 76.60\* | 14.16 | 11.09 | 9.11 | 7.73 | 6.70 | 5.92 | 5.31\* | 4.82 | 4.84 | 4.84 |
| BSL sin10 | 4.85 | 4.85 | 4.84 | 4.85 | 4.84 | 4.84 | 4.85 | 4.85 | 4.85\* | 4.85\* | 4.85\* |
At thd = 0, Skip gives a huge win. For high thd, bsl is selected correctly.
But now on sparse masks, expand for empty registers is wasted.
### pilot v3
New detour version: during compress, we'll store only the indices of non-empty registers (into the `idx` buffer).
And expand will iterate over them:
template <bool Skip>
size_t detour_compact(float* dst, const size_t n, const auto w, auto f, auto mask) {
float* ptr = tmp.data();
size_t k = 0;
for (size_t i = 0; i < n; i += 16) {
// ... same as detour
if constexpr (Skip)
if (vmaxvq_u32(vaddq_u32(vaddq_u32(m[0], m[1]), vaddq_u32(m[2], m[3]))) == 0) continue;
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) sk[j] = vaddvq_u32(vandq_u32(m[j], w));
for (size_t j = 0; j < 4; ++j) {
s[k] = sk[j];
idx[k] = i + 4 * j;
k += bool(sk[j]);
}
// ... same as detour
}
// ... same as detour
for (size_t j = 0; j < 3; ++j) s[k + j] = 0, idx[k + j] = 0;
// ... same as detour
k = (k + 3) & ~size_t(3);
for (size_t i = 0; i < k; i += 4) {
std::array<uint32_t, 4> sk;
for (size_t j = 0; j < 4; ++j) sk[j] = s[i + j];
std::array<float32x4_t, 4> a;
for (size_t j = 0; j < 4; ++j) a[j] = vld1q_f32(dst + idx[i + j]);
// ... same as detour
for (size_t j = 0; j < 4; ++j)
vst1q_f32(dst + idx[i + j], vreinterpretq_f32_u8(vqtbl2q_u8(tbl[j], index[j])));
ptr += off[3] + (sk[3] >> 4);
}
return size;
}
k (number of non-empty registers) is rounded up to a multiple of 4 before expand,
so the unrolled loop has no tail left.
No branches in the hot loops: they'd kill speed, so compress runs on all four registers.
Instead of branches, the position of the current element is advanced by `bool(sk)`.
detour_compact wins when at least 50% of all registers are empty. The density is approximately 0.16 (`(1 - x) ^ 4 = 0.5`).
void pilot_v3(float* dst, const size_t n, auto f, auto mask) {
// ... same as v2
constexpr size_t lo = 0.16 * probe;
for (size_t i = 2 * tile; i < n; i += tile) {
const long long cnt = density(dst, probe, mask);
if (cnt > hi) {
if (cnt < xlo)
bsl<true>(dst, tile, f, mask);
else
bsl<false>(dst, tile, f, mask);
} else if (cnt < xlo)
detour_compact<true>(dst, tile, w, f, mask);
else if (cnt < lo)
detour_compact<false>(dst, tile, w, f, mask);
else
detour<false>(dst, tile, w, f, mask);
dst += tile;
}
}
And v3 is noticeably faster at low density:
| thd | 0 | 0.02 | 0.04 | 0.06 | 0.08 | 0.1 | 0.12 | 0.14 | 0.16 | 0.18 | 0.2 |
|:---------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|
| pilot_v2 sin35 | 77.29\* | 18.67 | 18.40 | 17.70 | 17.18 | 16.66 | 16.15 | 15.68 | 15.24\* | 14.82\* | 14.43\* |
| pilot_v3 sin35 | 77.26 | 23.71\* | 22.19\* | 20.48\* | 19.03\* | 17.82\* | 16.73\* | 15.76\* | 15.16 | 14.74 | 14.38 |
| pilot_v2 pow10 | 72.88 | 13.93 | 11.32 | 9.38 | 8.00 | 6.97 | 6.18 | 5.54 | 5.02\* | 4.59\* | 4.24\* |
| pilot_v3 pow10 | 73.71\* | 16.54\* | 12.63\* | 10.07\* | 8.39\* | 7.17\* | 6.26\* | 5.55\* | 5.02\* | 4.59\* | 4.24\* |
Now the algo is fast,
but there's one big problem we've overlooked: we're assuming the data is uniform.
So it's easy to build a test where v3 will fail:
for (size_t i = 0; i < n; i++)
dst[i] = i % 4096 >= 256;
In this case, v3 always prefers BSL, even though detour wins on 3840 elements of the tile.
### pilot v3.5
According to the first table, in the worst case detour is under 3x slower (it happens on sqrt thd = 1), but on pow,
thd = 0, detour is 37x faster.
So a wrong BSL costs much more than a wrong detour.
For bsl, we'll play it safe by running it in tile/8 blocks and checking the density. If it drops well below `d_max`,
we'll switch to detour.
It costs 1 instruction per register. `acc = vsubq_u32(acc, m)` works because the mask is 0/-1. And unlike the probe, the
density here is exact.
size_t bsl_verified(float* dst, const size_t n, float d_max, auto f, auto mask) {
for (size_t i0 = 0; i0 < 8; ++i0) {
std::array<uint32x4_t, 4> acc;
acc.fill(vdupq_n_u32(0));
for (size_t i = 0; i < n / 8; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + 4 * j);
std::array<uint32x4_t, 4> m;
for (size_t j = 0; j < 4; ++j) m[j] = mask(v[j]);
for (size_t j = 0; j < 4; ++j) acc[j] = vsubq_u32(acc[j], m[j]);
for (size_t j = 0; j < 4; ++j)
vst1q_f32(dst + 4 * j, vbslq_f32(m[j], f(v[j]), v[j]));
dst += 16;
}
const size_t cur = vaddvq_u32(vaddq_u32(vaddq_u32(acc[0], acc[1]), vaddq_u32(acc[2], acc[3])));
if (static_cast<double>(cur) / (n / 8) < 0.75 * d_max) return (i0 + 1) * n / 8;
}
return n;
}
BSL bails out when the density is less than `0.75 * d_max`. I have no math behind the 0.75, it just won on average.
And pilot v3.5 will use bsl_verified if the density > hi:
void pilot_v3_5(float* dst, const size_t n, auto f, auto mask) {
// ... same as v3
for (size_t i = 2 * tile; i < n; i += tile) {
const long long cnt = density(dst, probe, mask);
if (cnt > hi) {
if (hi < 0) {
if (cnt < xlo)
bsl<true>(dst, tile, f, mask);
else
bsl<false>(dst, tile, f, mask);
} else {
auto done = bsl_verified(dst, tile, d_max, f, mask);
if (done < tile)
detour<false>(dst + done, tile - done, w, f, mask);
}
}
// ... same as v3
}
}
But here too it's easy to build a countertest:
for (size_t i = 0; i < n; ++i)
dst[i] = i % 4096 >= 390;
The first block will pass the check (> 75% zeros in it), but the second won't. BSL runs on it for nothing, and for pow
that's expensive. v3.5's problem: it has no memory.
After a miss the algo keeps trusting the first 256 and misses on every tile.
### pilot v4
v4 will fix this: if bsl bails out, we stop trusting the probe for the next 16 tiles, and instead we take the density of
the previous tile:
void pilot_v4(float* dst, const size_t n, auto f, auto mask) {
// ... same as v3
size_t distrust = 0;
size_t prev = 0;
for (size_t i = 2 * tile; i < n; i += tile) {
if (distrust) --distrust;
const long long cnt = distrust ? prev : density(dst, probe, mask);
if (cnt > hi) {
if (hi < 0) {
if (cnt < xlo) {
bsl<true>(dst, tile, f, mask);
} else {
bsl<false>(dst, tile, f, mask);
}
} else {
if (distrust == 0) {
auto done = bsl_verified(dst, tile, d_max, f, mask);
if (done < tile) {
distrust = 16;
const size_t rem =
So it's easy to build a test where v3 will fail:
for (size_t i = 0; i < n; i++)
dst[i] = i % 4096 >= 256;
In this case, v3 always prefers BSL, even though detour wins on 3840 elements of the tile.
### pilot v3.5
According to the first table, in the worst case detour is under 3x slower (it happens on sqrt thd = 1), but on pow,
thd = 0, detour is 37x faster.
So a wrong BSL costs much more than a wrong detour.
For bsl, we'll play it safe by running it in tile/8 blocks and checking the density. If it drops well below `d_max`,
we'll switch to detour.
It costs 1 instruction per register. `acc = vsubq_u32(acc, m)` works because the mask is 0/-1. And unlike the probe, the
density here is exact.
size_t bsl_verified(float* dst, const size_t n, float d_max, auto f, auto mask) {
for (size_t i0 = 0; i0 < 8; ++i0) {
std::array<uint32x4_t, 4> acc;
acc.fill(vdupq_n_u32(0));
for (size_t i = 0; i < n / 8; i += 16) {
std::array<float32x4_t, 4> v;
for (size_t j = 0; j < 4; ++j) v[j] = vld1q_f32(dst + 4 * j);
std::array<uint32x4_t, 4> m;
for (size_t j = 0; j < 4; ++j) m[j] = mask(v[j]);
for (size_t j = 0; j < 4; ++j) acc[j] = vsubq_u32(acc[j], m[j]);
for (size_t j = 0; j < 4; ++j)
vst1q_f32(dst + 4 * j, vbslq_f32(m[j], f(v[j]), v[j]));
dst += 16;
}
const size_t cur = vaddvq_u32(vaddq_u32(vaddq_u32(acc[0], acc[1]), vaddq_u32(acc[2], acc[3])));
if (static_cast<double>(cur) / (n / 8) < 0.75 * d_max) return (i0 + 1) * n / 8;
}
return n;
}
BSL bails out when the density is less than `0.75 * d_max`. I have no math behind the 0.75, it just won on average.
And pilot v3.5 will use bsl_verified if the density > hi:
void pilot_v3_5(float* dst, const size_t n, auto f, auto mask) {
// ... same as v3
for (size_t i = 2 * tile; i < n; i += tile) {
const long long cnt = density(dst, probe, mask);
if (cnt > hi) {
if (hi < 0) {
if (cnt < xlo)
bsl<true>(dst, tile, f, mask);
else
bsl<false>(dst, tile, f, mask);
} else {
auto done = bsl_verified(dst, tile, d_max, f, mask);
if (done < tile)
detour<false>(dst + done, tile - done, w, f, mask);
}
}
// ... same as v3
}
}
But here too it's easy to build a countertest:
for (size_t i = 0; i < n; ++i)
dst[i] = i % 4096 >= 390;
The first block will pass the check (> 75% zeros in it), but the second won't. BSL runs on it for nothing, and for pow
that's expensive. v3.5's problem: it has no memory.
After a miss the algo keeps trusting the first 256 and misses on every tile.
### pilot v4
v4 will fix this: if bsl bails out, we stop trusting the probe for the next 16 tiles, and instead we take the density of
the previous tile:
void pilot_v4(float* dst, const size_t n, auto f, auto mask) {
// ... same as v3
size_t distrust = 0;
size_t prev = 0;
for (size_t i = 2 * tile; i < n; i += tile) {
if (distrust) --distrust;
const long long cnt = distrust ? prev : density(dst, probe, mask);
if (cnt > hi) {
if (hi < 0) {
if (cnt < xlo) {
bsl<true>(dst, tile, f, mask);
} else {
bsl<false>(dst, tile, f, mask);
}
} else {
if (distrust == 0) {
auto done = bsl_verified(dst, tile, d_max, f, mask);
if (done < tile) {
distrust = 16;
const size_t rem =
detour<false>(dst + done, tile - done, w, f, mask);
prev = rem * probe / (tile - done);
}
} else {
prev = detour<false>(dst, tile, w, f, mask) * probe / tile;
}
}
} else if (cnt < xlo) {
prev = detour_compact<true>(dst, tile, w, f, mask) * probe / tile;
} else if (cnt < lo) {
prev = detour_compact<false>(dst, tile, w, f, mask) * probe / tile;
} else {
prev = detour<false>(dst, tile, w, f, mask) * probe / tile;
}
dst += tile;
}
}
And v4 easily passes that test. pow10:
| thd | 0 | 1 |
|--------------|--------:|-------:|
| tiled detour | 38.18 | 6.94 |
| pilot v3 | 68.11 | 1.04 |
| pilot v3.5 | 68.67 | 3.84 |
| pilot v4 | 69.51\* | 7.05\* |
| BSL | 1.04 | 1.04 |
btw here thd no longer matches the density. For thd = 0, density = 0, and for thd = 1, density = 9.5%
It beats v3.5 by over 80%. It's also faster than plain tiled detour, because v4 picks detour_compact. This is the final
version.
## Results
v4 vs BSL:
| thd | 0 | 0.05 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 |
|:-----------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|
| pilot_v4 sqrt | 69.81\* | 31.99\* | 31.84 | 31.73 | 31.83 | 32.07\* | 31.98 | 31.96\* | 31.96 | 31.89\* | 31.91 | 31.77 |
| BSL sqrt | 31.88 | 31.93 | 31.95\* | 31.80\* | 31.89\* | 31.99 | 32.06\* | 31.92 | 31.98\* | 31.75 | 31.93\* | 31.91\* |
| pilot_v4 frfrexp | 69.41\* | 17.21\* | 16.57 | 15.79 | 15.99 | 16.06 | 15.93 | 15.99 | 16.39 | 15.84 | 15.87 | 15.80 |
| BSL frfrexp | 16.79 | 16.87 | 16.82\* | 16.71\* | 16.78\* | 16.88\* | 16.89\* | 16.83\* | 16.89\* | 16.79\* | 16.75\* | 16.75\* |
| pilot_v4 sin10 | 68.19\* | 18.82\* | 14.84\* | 10.84\* | 8.93\* | 7.55\* | 6.56\* | 5.81\* | 5.19\* | 4.69 | 4.68 | 4.68 |
| BSL sin10 | 4.78 | 4.75 | 4.78 | 4.72 | 4.75 | 4.74 | 4.75 | 4.77 | 4.75 | 4.76\* | 4.77\* | 4.77\* |
| pilot_v4 pow10 | 64.17\* | 10.76\* | 6.85\* | 4.04\* | 2.89\* | 2.25\* | 1.85\* | 1.57\* | 1.36\* | 1.20\* | 1.08\* | 1.00 |
| BSL pow10 | 1.02 | 1.02 | 1.02 | 1.01 | 1.01 | 1.01 | 1.02 | 1.02 | 1.02 | 1.02 | 1.02 | 1.01\* |
Against BSL, it loses at worst 6%, but wins big much more often.
To reduce the loss, dispatch can be sped up: use every 4th register in density and bsl_verified. But that helps only if
the density is uniform.
The worst v4 miss I found: 512 dense, 512 empty, then everything is dense until the end of the cycle (17 * 4096).
const size_t cycle = 17 * 4096;
for (size_t i = 0; i < n; ++i) {
dst[i] = i % cycle >= 512 && i % cycle < 1024;
}
This hurts most with the cheapest `f` (with `d_max > 0`, of course):
const auto a = vdupq_n_f32(0.5f);
#pragma unroll
for (int i = 0; i < 13; ++i) x = vfmaq_f32(a, x, a);
return x;
| thd | 0 | 1 |
|--------------|--------:|--------:|
| tiled detour | 38.29 | 9.44 |
| pilot v3 | 74.72 | 15.81\* |
| pilot v3.5 | 74.84 | 15.04 |
| pilot v4 | 74.89\* | 9.84 |
| BSL | 15.61 | 15.60 |
v3.5's biggest loss is limited by BSL, while v4 is limited by detour. And a wrong detour is the cheaper mistake.
So v4 isn't always better, but its misses are less severe.
This problem has no perfect solution. Any dispatch algo can be countertested.
Full code: [godbolt](https://godbolt.org/z/nKhPjEn6v).
https://redd.it/1v94b7w
@r_cpp
prev = rem * probe / (tile - done);
}
} else {
prev = detour<false>(dst, tile, w, f, mask) * probe / tile;
}
}
} else if (cnt < xlo) {
prev = detour_compact<true>(dst, tile, w, f, mask) * probe / tile;
} else if (cnt < lo) {
prev = detour_compact<false>(dst, tile, w, f, mask) * probe / tile;
} else {
prev = detour<false>(dst, tile, w, f, mask) * probe / tile;
}
dst += tile;
}
}
And v4 easily passes that test. pow10:
| thd | 0 | 1 |
|--------------|--------:|-------:|
| tiled detour | 38.18 | 6.94 |
| pilot v3 | 68.11 | 1.04 |
| pilot v3.5 | 68.67 | 3.84 |
| pilot v4 | 69.51\* | 7.05\* |
| BSL | 1.04 | 1.04 |
btw here thd no longer matches the density. For thd = 0, density = 0, and for thd = 1, density = 9.5%
It beats v3.5 by over 80%. It's also faster than plain tiled detour, because v4 picks detour_compact. This is the final
version.
## Results
v4 vs BSL:
| thd | 0 | 0.05 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 |
|:-----------------|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|--------:|
| pilot_v4 sqrt | 69.81\* | 31.99\* | 31.84 | 31.73 | 31.83 | 32.07\* | 31.98 | 31.96\* | 31.96 | 31.89\* | 31.91 | 31.77 |
| BSL sqrt | 31.88 | 31.93 | 31.95\* | 31.80\* | 31.89\* | 31.99 | 32.06\* | 31.92 | 31.98\* | 31.75 | 31.93\* | 31.91\* |
| pilot_v4 frfrexp | 69.41\* | 17.21\* | 16.57 | 15.79 | 15.99 | 16.06 | 15.93 | 15.99 | 16.39 | 15.84 | 15.87 | 15.80 |
| BSL frfrexp | 16.79 | 16.87 | 16.82\* | 16.71\* | 16.78\* | 16.88\* | 16.89\* | 16.83\* | 16.89\* | 16.79\* | 16.75\* | 16.75\* |
| pilot_v4 sin10 | 68.19\* | 18.82\* | 14.84\* | 10.84\* | 8.93\* | 7.55\* | 6.56\* | 5.81\* | 5.19\* | 4.69 | 4.68 | 4.68 |
| BSL sin10 | 4.78 | 4.75 | 4.78 | 4.72 | 4.75 | 4.74 | 4.75 | 4.77 | 4.75 | 4.76\* | 4.77\* | 4.77\* |
| pilot_v4 pow10 | 64.17\* | 10.76\* | 6.85\* | 4.04\* | 2.89\* | 2.25\* | 1.85\* | 1.57\* | 1.36\* | 1.20\* | 1.08\* | 1.00 |
| BSL pow10 | 1.02 | 1.02 | 1.02 | 1.01 | 1.01 | 1.01 | 1.02 | 1.02 | 1.02 | 1.02 | 1.02 | 1.01\* |
Against BSL, it loses at worst 6%, but wins big much more often.
To reduce the loss, dispatch can be sped up: use every 4th register in density and bsl_verified. But that helps only if
the density is uniform.
The worst v4 miss I found: 512 dense, 512 empty, then everything is dense until the end of the cycle (17 * 4096).
const size_t cycle = 17 * 4096;
for (size_t i = 0; i < n; ++i) {
dst[i] = i % cycle >= 512 && i % cycle < 1024;
}
This hurts most with the cheapest `f` (with `d_max > 0`, of course):
const auto a = vdupq_n_f32(0.5f);
#pragma unroll
for (int i = 0; i < 13; ++i) x = vfmaq_f32(a, x, a);
return x;
| thd | 0 | 1 |
|--------------|--------:|--------:|
| tiled detour | 38.29 | 9.44 |
| pilot v3 | 74.72 | 15.81\* |
| pilot v3.5 | 74.84 | 15.04 |
| pilot v4 | 74.89\* | 9.84 |
| BSL | 15.61 | 15.60 |
v3.5's biggest loss is limited by BSL, while v4 is limited by detour. And a wrong detour is the cheaper mistake.
So v4 isn't always better, but its misses are less severe.
This problem has no perfect solution. Any dispatch algo can be countertested.
Full code: [godbolt](https://godbolt.org/z/nKhPjEn6v).
https://redd.it/1v94b7w
@r_cpp
godbolt.org
Compiler Explorer - C++ (armv8-a clang (trunk))
consteval auto compress_table() {
std::array<std::array<uint8_t, 16>, 16> index;
std::array<uint8_t, 16> row;
row.fill(255);
index.fill(row);
for (size_t idx = 0; idx < 16; ++idx) {
size_t j = 0;
for (size_t i = 0; i <…
std::array<std::array<uint8_t, 16>, 16> index;
std::array<uint8_t, 16> row;
row.fill(255);
index.fill(row);
for (size_t idx = 0; idx < 16; ++idx) {
size_t j = 0;
for (size_t i = 0; i <…
Implementing the Tuple API easily using custom annotations and reflections
Hey, so I've been working on something interesting lately. It's a solution to a problem that you may have encountered in one form or anothing but it boils down to this: you have a struct:
struct mystruct
{
long a;
int b;
char c;
};
And you want this to work:
auto [a, b, c] = instance;
Well it doesn't, aggregates don't implement the tuple API required for structured bindings by default and implementing it manually is a pain. You need to write your own specialization of std::tuple\size, std::tuple_element and implement a visible get<> method. It might not be that much work at first glance but it becomes very tiresome if you have multiple such structs or if you try to deviate from the general default indexing and not to mention a paint to maintain and update.
Well, why write repeated code when you can only write code once that writes that code for you? Reflections are mature enough in gcc's implementation to solve this. Simply put I have compiling code that with only the application of an annotation gives me all of this:
struct [=tuple_like] simpletuple
{
long a;
int b;
char c;
};
Through which I get:
staticassert(std::tuplesizev<simpletuple> == 3);
constexpr simpletuple simple { 1, 2, 3 };
staticassert(1 == get<0>(simple));
staticassert(2 == get<1>(simple));
staticassert(3 == get<2>(simple));
staticassert(std::sameas<std::tupleelementt<0, simpletuple>, long>);
staticassert(std::sameas<std::tupleelementt<1, simpletuple>, int>);
staticassert(std::sameas<std::tupleelementt<2, simpletuple>, char>);
And of couse:
auto a, b, c = simple;
It doesn't stop here though, since the case I described above is common but inflexible. Customization is available and an opt-in feature:
struct [=tuple_like] customizabletuple
{
constexpr customizabletuple(long a, const char b, float c, char d, double e)
: a(a)
, b(b)
, c(c)
, d(d)
, e(e)
{}
[[=tuple_element(0)]]
long a;
[[=read_only_tuple_element(3)]]
const char b;
[=tuple_element(2)]
float c;
char d;
private:
[=read_only_tuple_element(1)]
double e;
};
It supports:
tuple element index reordering
implicit data member exclusion from the API
explicit read-only modifier
explicit opt-in for private fields
correct semantics for members which are reference types
​
constexpr customizable_tuple custom( 1, "2", 3, 4, 5 );
static_assert(std::tuple_size_v<customizable_tuple> == 4);
static_assert(1 == get<0>(custom));
static_assert(std::string_view{"2"} == get<3>(custom));
static_assert(3 == get<2>(custom));
static_assert(5 == get<1>(custom));
static_assert(std::same_as<std::tuple_element_t<0, customizable_tuple>, long>);
static_assert(std::same_as<std::tuple_element_t<1, customizable_tuple>, const double>);
static_assert(std::same_as<std::tuple_element_t<2, customizable_tuple>, float>);
static_assert(std::same_as<std::tuple_element_t<3, customizable_tuple>, const char const>);
But it doesn't stop there, no it doesn't. The part that took the most effort to implement were the error messages. No, we have better static_assert's in C++26 now, we abuse them:
Example 1:
staticassert(3 == get<3>(simple));
/app/myreflectutils.hpp:819:17:
error:
static assertion failed: reflectutils: get<3> called for tuple-like type 'simpletuple', but its reflected tuple size is 3.
Example 2:
[[=tupleelement(1)]]
long a;
[=tuple_element(2)]
int b;
[=tuple_element(3)]
char c;
/app/myreflectutils.hpp:632:21: error: static assertion failed: reflectutils: invalid tuple layout for 'simpletuple': tuple index 0 is missing. There are 3
Hey, so I've been working on something interesting lately. It's a solution to a problem that you may have encountered in one form or anothing but it boils down to this: you have a struct:
struct mystruct
{
long a;
int b;
char c;
};
And you want this to work:
auto [a, b, c] = instance;
Well it doesn't, aggregates don't implement the tuple API required for structured bindings by default and implementing it manually is a pain. You need to write your own specialization of std::tuple\size, std::tuple_element and implement a visible get<> method. It might not be that much work at first glance but it becomes very tiresome if you have multiple such structs or if you try to deviate from the general default indexing and not to mention a paint to maintain and update.
Well, why write repeated code when you can only write code once that writes that code for you? Reflections are mature enough in gcc's implementation to solve this. Simply put I have compiling code that with only the application of an annotation gives me all of this:
struct [=tuple_like] simpletuple
{
long a;
int b;
char c;
};
Through which I get:
staticassert(std::tuplesizev<simpletuple> == 3);
constexpr simpletuple simple { 1, 2, 3 };
staticassert(1 == get<0>(simple));
staticassert(2 == get<1>(simple));
staticassert(3 == get<2>(simple));
staticassert(std::sameas<std::tupleelementt<0, simpletuple>, long>);
staticassert(std::sameas<std::tupleelementt<1, simpletuple>, int>);
staticassert(std::sameas<std::tupleelementt<2, simpletuple>, char>);
And of couse:
auto a, b, c = simple;
It doesn't stop here though, since the case I described above is common but inflexible. Customization is available and an opt-in feature:
struct [=tuple_like] customizabletuple
{
constexpr customizabletuple(long a, const char b, float c, char d, double e)
: a(a)
, b(b)
, c(c)
, d(d)
, e(e)
{}
[[=tuple_element(0)]]
long a;
[[=read_only_tuple_element(3)]]
const char b;
[=tuple_element(2)]
float c;
char d;
private:
[=read_only_tuple_element(1)]
double e;
};
It supports:
tuple element index reordering
implicit data member exclusion from the API
explicit read-only modifier
explicit opt-in for private fields
correct semantics for members which are reference types
​
constexpr customizable_tuple custom( 1, "2", 3, 4, 5 );
static_assert(std::tuple_size_v<customizable_tuple> == 4);
static_assert(1 == get<0>(custom));
static_assert(std::string_view{"2"} == get<3>(custom));
static_assert(3 == get<2>(custom));
static_assert(5 == get<1>(custom));
static_assert(std::same_as<std::tuple_element_t<0, customizable_tuple>, long>);
static_assert(std::same_as<std::tuple_element_t<1, customizable_tuple>, const double>);
static_assert(std::same_as<std::tuple_element_t<2, customizable_tuple>, float>);
static_assert(std::same_as<std::tuple_element_t<3, customizable_tuple>, const char const>);
But it doesn't stop there, no it doesn't. The part that took the most effort to implement were the error messages. No, we have better static_assert's in C++26 now, we abuse them:
Example 1:
staticassert(3 == get<3>(simple));
/app/myreflectutils.hpp:819:17:
error:
static assertion failed: reflectutils: get<3> called for tuple-like type 'simpletuple', but its reflected tuple size is 3.
Example 2:
[[=tupleelement(1)]]
long a;
[=tuple_element(2)]
int b;
[=tuple_element(3)]
char c;
/app/myreflectutils.hpp:632:21: error: static assertion failed: reflectutils: invalid tuple layout for 'simpletuple': tuple index 0 is missing. There are 3
explicitly annotated members, so their indices must be exactly 0, 3). Member 'simple_tuple::c' uses index 3.
Example 3:
[[=tuple_element(0)]
[=tuple_element(1)]
long a;
/app/myreflectutils.hpp:632:21: error: static assertion failed: reflectutils: invalid tuple layout for 'simpletuple': member 'simpletuple::a' has 2 tuple annotations; use exactly one of [[=tupleelement(i)]] and [=read_only_tuple_element(i)].
Full code: https://godbolt.org/z/exjW6YGEr
https://redd.it/1v9u2d5
@r_cpp
Example 3:
[[=tuple_element(0)]
[=tuple_element(1)]
long a;
/app/myreflectutils.hpp:632:21: error: static assertion failed: reflectutils: invalid tuple layout for 'simpletuple': member 'simpletuple::a' has 2 tuple annotations; use exactly one of [[=tupleelement(i)]] and [=read_only_tuple_element(i)].
Full code: https://godbolt.org/z/exjW6YGEr
https://redd.it/1v9u2d5
@r_cpp
godbolt.org
Compiler Explorer - C++ (x86-64 gcc (trunk))
using namespace reflect_utils;
struct [[=tuple_like]] simple_tuple
{
long a;
int b;
char c;
};
static_assert(std::tuple_size_v<simple_tuple> == 3);
constexpr simple_tuple simple { 1, 2, 3 };
static_assert(1 == get<0>(simple));
static_assert(2…
struct [[=tuple_like]] simple_tuple
{
long a;
int b;
char c;
};
static_assert(std::tuple_size_v<simple_tuple> == 3);
constexpr simple_tuple simple { 1, 2, 3 };
static_assert(1 == get<0>(simple));
static_assert(2…
C++26: Reducing undefined behaviour
https://www.sandordargo.com/blog/2026/07/29/cpp26-reduces-undefined-behaviour
https://redd.it/1v9x7nt
@r_cpp
https://www.sandordargo.com/blog/2026/07/29/cpp26-reduces-undefined-behaviour
https://redd.it/1v9x7nt
@r_cpp
Sandor Dargo’s Blog
C++26: Reducing undefined behaviour
Nowadays, safety is in the focus of C++. Whether it’s at committee meetings, conference talks, or hallway discussions, the topic keeps coming up. C++26 made some big steps forward in this area, and one recurring theme is the reduction of cases leading to…
Building a compiler that works at compile-time so you can compile your program while you compile your program.
In short, I wanted to build a compiler of some C subset that would work at compile-time. It compiles into a custom byte-code for a runtime VM.
I've once tried to write a compile-time C compiler, but I abandoned that project, because I made it overly complex (one-pass compiler right into x86). No clear separation between parser, lexer, etc.
Why would I even want this? Idk. But how can it be useful?
- The code of the compiler doesn't go to the resulting binary,
- No need to waste time for compilation at runtime too,
- Guaranteed type-safety. There can't be such thing as "oh, I changed the function signature, but forgot to update the bindings and it crashed at runtime"
And I shouldn't forget about cons:
- No optimisations. Real compilers spent decades on them and I'm definitely not going to implement LLVM at runtime. Although we could make a compile-time x86 VM, so we can run it at compile time... no, thank you, it's a topic for another fever dream article.
- Hot-reload! I mean, no hot-reload. I won't even mention it anymore, considering that the script is compiled at compile-time and is builtin right into the binary file. I could implement it with hot memory patching or smth, but who really needs it.
Let's start.
## Bypassing constexpr limitations
C++ 20 lets us to dynamically allocate memory at compile-time and even use
### Passing strings in templates
Sadly, the C++ Committee made a lot of cool compile-time features, but not enough (at least for me). We still can't use strings in templates without hacks. But we can easily bypass it with a well-known trick.
### Extracting vectors from compile time
It turned out to be not really that hard, but I didn't really find any ready examples on Internet, unlike with
To extract
I thought of passing vector as a template parameter, but we can't do it legally. C++ 20 allows us to pass only the structs with all-public members. After deeply thinking a bit (not really), I discovered that I could simply pass the lambda that returns our vector (I didn't think I could just pass a pointer actually).
### Printing errors
For nice errors C++ has
In short, I wanted to build a compiler of some C subset that would work at compile-time. It compiles into a custom byte-code for a runtime VM.
I've once tried to write a compile-time C compiler, but I abandoned that project, because I made it overly complex (one-pass compiler right into x86). No clear separation between parser, lexer, etc.
Why would I even want this? Idk. But how can it be useful?
- The code of the compiler doesn't go to the resulting binary,
- No need to waste time for compilation at runtime too,
- Guaranteed type-safety. There can't be such thing as "oh, I changed the function signature, but forgot to update the bindings and it crashed at runtime"
And I shouldn't forget about cons:
- No optimisations. Real compilers spent decades on them and I'm definitely not going to implement LLVM at runtime. Although we could make a compile-time x86 VM, so we can run it at compile time... no, thank you, it's a topic for another fever dream article.
- Hot-reload! I mean, no hot-reload. I won't even mention it anymore, considering that the script is compiled at compile-time and is builtin right into the binary file. I could implement it with hot memory patching or smth, but who really needs it.
Let's start.
## Bypassing constexpr limitations
C++ 20 lets us to dynamically allocate memory at compile-time and even use
std::vector that really expands our borders. But there's one very important note - you can't declare a compile-time vector and extract it into the runtime. No constexpr std::vector<int> data = makeData();, it won't compile. So we need to hack it.### Passing strings in templates
Sadly, the C++ Committee made a lot of cool compile-time features, but not enough (at least for me). We still can't use strings in templates without hacks. But we can easily bypass it with a well-known trick.
template<std::size_t N>
struct const_string {
constexpr const_string() = default;
// implicit-constructor that lets us to do bad things
constexpr const_string(const char (&str)[N]) {
std::copy_n(str, N, value);
}
constexpr operator std::string_view() const {
return {value, value + N - 1};
}
char value[N]{};
const std::size_t length = N;
};
// using it
template<const_string str>
auto very_smart_function(...) { /* ... */ }
### Extracting vectors from compile time
It turned out to be not really that hard, but I didn't really find any ready examples on Internet, unlike with
const_string.To extract
std::vector<T> from constexpr we need to make it std::array<T, N> somehow. The main problem is that we can't write std::array<T, myVector.size()>, because myVector.size() won't be a constant value. So we must to make it constant somehow.I thought of passing vector as a template parameter, but we can't do it legally. C++ 20 allows us to pass only the structs with all-public members. After deeply thinking a bit (not really), I discovered that I could simply pass the lambda that returns our vector (I didn't think I could just pass a pointer actually).
// data_getter is our lambda
template<auto data_getter>
constexpr auto to_array() {
using value_type = typename decltype(data_getter())::value_type;
constexpr static std::size_t size = data_getter().size();
// Create a static array with a "dynamic" size and copy all data
std::array<value_type, size> out;
auto in = data_getter();
for (std::size_t i = 0; i < size; ++i) {
out[i] = in[i];
}
return out; // yay
}
template<const_string str>
constexpr auto lex() {
constexpr static auto data_getter = [] constexpr {
// .lex() returns the vector of tokens
return lexer{static_cast<std::string_view>(str)}.lex();
};
// All our data are available for runtime now =D
return to_array<data_getter>();
}
### Printing errors
For nice errors C++ has
static_assert that allows us to even print our custom message! But it must be always a literal (until C++