struct Fraction` โ default now `int64_t`. โ
---
### ๐ Fix #2 โ Move PyFred's gcd Out of the Class Template
The gcd became **free, non-template functions** in `fractions::detail`:
```cpp
namespace fractions {
namespace detail {
CONSTEXPR14 auto gcd_recur(long long _m, long long _n) -> long long {
if (_n == 0) return abs(_m);
return gcd_recur(_n, _m % _n); // plain overload, not a template
}
CONSTEXPR14 auto gcd(long long _m, long long _n) -> long long {
if (_m == 0) return abs(_n);
return gcd_recur(_m, _n);
}
```
- The class body now calls `detail::gcd(_numerator, _denominator)`. ๐
- No `T` in any gcd signature โ nothing left to prevent unrolling. ๐ฏ
- Kept in `detail` so it never collides with `fractions::gcd`. ๐งท
.mermaid[
graph LR
C["Fraction<int64_t>\nnormalize()"] --> D["detail::gcd(long long, long long)"]
D --> R["detail::gcd_recur(long long, long long)"]
R -->|"m % n"| R
style C fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px
style D fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
style R fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
]
---
### ๐ฌ Evidence โ g++ `-O3` Assembly of the Constructor
> ๐งช `xmake test`: **137 cases / 958 assertions** pass; `clang-format --dry-run -Werror` clean.
---
class: nord-light, middle, center
## โก Part 4 โ Six Optimizations
---
### 1๏ธโฃ `operator+`: Skip the Constructor gcd
```cpp
auto denom = da * right;
auto numer = right * na + left * nb;
if (common == 1) {
return Fraction::from_raw(numer, denom); // already reduced
}
const auto g = gcd(numer, common); // only factors of common
return Fraction::from_raw(numer / g, denom / g);
```
$$ \frac{{\color{#1565c0}a}}{{\color{#2e7d32}b}}+\frac{{\color{#b48ead}c}}{{\color{#d08770}d}}
=\frac{{\color{#1565c0}a}{\color{#d08770}d}+{\color{#2e7d32}b}{\color{#b48ead}c}}
{{\color{#2e7d32}b}{\color{#d08770}d}} $$
If $\gcd(b,d)=1$, the sum is **already in lowest terms**. โ
> ๐งพ The old `operator+` ran a **second** gcd in the constructor.
---
### 2๏ธโฃ Integer ยฑ : Zero gcds
Because $a/b$ is canonical:
$$ \gcd({\color{#1565c0}a} \pm {\color{#bf616a}n}\,{\color{#2e7d32}b},\; {\color{#2e7d32}b})
\;=\; \gcd({\color{#1565c0}a},{\color{#2e7d32}b}) \;=\; 1 $$
```cpp
// before: swap + reduce + swap + reduce
// after:
this->_numer += rhs * this->_denom; // no gcd at all
```
- Overflow behaviour unchanged: the old code already formed $n \cdot b$. ๐งฎ
- PyFraction still routes `+ int` through a full fraction add โ the big outlier. ๐
---
### 3๏ธโฃ Scalar Multiply: One gcd, Not Two
```cpp
std::swap(this->_numer, rhs);
this->reduce(); // gcd(rhs, denom)
this->_numer *= rhs;
this->keep_denom_positive(); // was normalize(): another gcd
```
After cross-reduction, the second gcd is **always 1**. โ
The same idea lets PyFraction build results with its private **skip-normalize** constructor:
```cpp
Fraction(T numerator, T denominator, bool skip_normalize);
```
> ๐ฆ `from_raw` (Simple) and `skip_normalize` (Py) are the same trick, two spellings.
---
### 4๏ธโฃ Comparison: Two Multiplies, Zero gcds
$$ \frac{\color{#1565c0}a}{\color{#2e7d32}b} \;<\; \frac{\color{#b48ead}c}{\color{#d08770}d}
\iff {\color{#1565c0}a}{\color{#d08770}d} \;<\; {\color{#b48ead}c}{\color{#2e7d32}b},
\qquad {\color{#2e7d32}b},{\color{#d08770}d}>0 $$
.mermaid[
graph LR
C["a/b vs c/d"] --> D{"both denominators > 0?"}
D -->|yes| W{"__SIZEOF_INT128__?"}
W -->|yes| I["__int128 cross product\nGCC ยท Clang ยท clang-cl"]
W -->|no, MSVC x64| U["_umul128 mirror\nhi/lo comparison"]
D -->|no: 0 or NaN| O["legacy reduce path"]
style C fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
style I fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
style U fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
style O fill:#fff9c4,stroke:#f57f17,stroke-width:3px
]
---
### 5๏ธโฃ MSVC: Replace Division-Based Overflow Checks
`_mul_overflows` used `max / |b|` โ one integer division **per check**, twice per multiply.
```cpp
const unsigned long long lo = _umul128(_abs_u64(a), _abs_u64(b), &hi);
const unsigned long long limit = negative ? positive_max + 1 : positive_max;
return hi != 0 || lo > limit;
```
- `_umul128` is exact and branch-light on MSVC x64. โ๏ธ
- Keeps the existing `double` fallback for genuine overflow. ๐
---
### 6๏ธโฃ Portability: `__int128` Under `-Wpedantic -Werror`
```cpp
#if defined(__SIZEOF_INT128__)
__extension__ typedef __int128 wide; // GCC/Clang extension, silenced
return static_cast(an) * bd < static_cast(bn) * ad;
#elif defined(_MSC_VER) && defined(_M_X64)
// _umul128 sign-magnitude comparison
#else
// legacy reduce-based path
#endif
```
- `using wide = __int128;` **fails** `-Wpedantic -Werror`. โ
- `__extension__ typedef` is clean. โ
- `` only when `_MSC_VER && _M_X64`. ๐ช
---
### ๐ What Did **Not** Work
---
### ๐ฆ Applied Patch
.font-sm[
| File | Change | Effect |
|:--|:--|:--|
| `fractions.hpp` | `operator+` uses `from_raw` | drop 2nd gcd |
| `fractions.hpp` | `+=/-=(T)` zero gcd | int ops ~10ร |
| `fractions.hpp` | `*=(T)` drops 2nd gcd | scalar mul ~2ร |
| `fractions.hpp` | `<` via `__int128` / `_umul128` | compare ~4ร |
| `fractions.hpp` | gcd โ **non-template overloads** | unrollable, `int64_t` default |
| `extfractions.hpp` | same arithmetic changes | parity with Simple |
| `pyfractions.hpp` | skip-normalize `+ - * /` | no re-normalize |
| `pyfractions.hpp` | `_umul128` overflow check | no per-check division |
| `pyfractions.hpp` | gcd โ free `fractions::detail` | no template recursion |
]
> ๐ Public API preserved (a defaulted `T` is additive). ๐งช Unit tests untouched.
---
class: nord-light, middle, center
## ๐ Part 5 โ Head-to-Head
---
### ๐ Fraction vs PyFraction โ median ns/op
.font-sm[
| Operation | ๐ g++ -O3 | ๐งโ๐ป clang-cl /O2 | ๐ช MSVC /O2 |
|:--|--:|--:|--:|
| construction | 35.95 / 35.93 | 21.62 / 21.61 | 28.76 / 28.79 |
| `a + b` large | 35.97 / **21.63** | 22.33 / **13.03** | 35.92 / **21.63** |
| `a * b` large | 35.95 / 36.09 | 21.61 / 21.60 | 35.94 / 36.16 |
| `a / b` small | 28.78 / **21.60** | 17.30 / **12.97** | 29.38 / **21.61** |
| `a < b` large | **2.64** / 3.18 | 5.04 / **4.73** | 7.21 / **6.48** |
| `a == b` | 1.46 / 1.46 | 2.17 / 2.16 | 2.16 / 2.16 |
| `a + 7` | **1.20** / 1.88 | **2.16** / 8.64 | **1.57** / 13.69 |
| `a * 7` | 14.42 / 14.41 | 8.66 / 8.65 | **15.12** / 21.70 |
| complex expr | 174.19 / 173.63 | **108.14** / 123.88 | **159.63** / 179.39 |
]
> ๐ PyFraction wins add/sub/div. ๐งฐ Fraction wins integer ยฑ and most comparisons. Construction is a tie.
---
### ๐ PyFraction / Fraction โ ratio (lower = Py faster)
---
### ๐งญ What Wins Where
.mermaid[
graph TD
Q["Which operation?"] --> F["fraction op: + - * /"]
Q --> I["fraction op with int"]
Q --> C["compare"]
F --> FW["PyFraction
add/sub/div ~0.58-0.75ร"]
I --> IW["Fraction
ยฑ int 1.3-8.7ร faster"]
C --> CW["Fraction on g++
PyFraction on clang-cl/MSVC"]
style Q fill:#eceff4,stroke:#4c566a,stroke-width:3px
style FW fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px
style IW fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
style CW fill:#fff9c4,stroke:#f57f17,stroke-width:3px
]
- ๐ **PyFraction**: fraction `+`, `-`, `/` win on every compiler; also `lt` on clang-cl/MSVC.
- ๐งฐ **Fraction**: integer `ยฑ` dominates everywhere; `lt` on g++; `complex` on clang-cl/MSVC.
- โ๏ธ **Ties**: construction, `*`, `==`.
- ๐ง Compiler-dependent โ the refactor made construction a tie; arithmetic paths still differ.
---
class: nord-light, middle, center
## ๐ Part 6 โ Size, Verification, Lessons
---
### ๐ Size: Lines of Code
| Header | total | code | comment | blank | size |
|--------|------:|-----:|--------:|------:|-----:|
| `fractions.hpp` ๐งฐ | 1545 | 497 | **937** | 111 | 54 KB |
| `extfractions.hpp` ๐ชถ | 1225 | 397 | 748 | 80 | 45 KB |
| `pyfractions.hpp` ๐ | 1127 | **647** | 334 | 146 | 41 KB |
- ๐ **PyFraction is the shortest file but the densest code** โ 1.3ร more executable lines than Fraction.
- ๐งฐ **Fraction is the longest file** โ Doxygen math diagrams (Graphviz + LaTeX) dominate; 497 lines are code.
- ๐ No public API removed; `Fraction` is additive.
> ๐ Fewer total lines โ less code. Compare *code* lines, not file length. ๐ง
---
### ๐งช Verification Pipeline
.mermaid[
graph TD
T["xmake test\n137 cases ยท 958 assertions"] --> OK["โ
"]
A["g++ -O3 assembly\n0 calls to gcd"] --> OK
D["bench: 3 passes\nmedian ns/op"] --> OK
G["GCC 15.2\n-Wpedantic -Wconversion -Werror"] --> OK
F["clang-format 22\n--dry-run --Werror"] --> OK
style OK fill:#c8e6c9,stroke:#2e7d32,stroke-width:4px
style T fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
style A fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px
style D fill:#fff9c4,stroke:#f57f17,stroke-width:3px
]
- Special cases: `0/0`, `ยฑ1/0`, negative denominators, both-negative comparisons. ๐งท
- Tests untouched; public API unchanged. ๐
- Benchmarks in one process โ no cross-binary noise. ๐
---
### ๐ Lessons
1. ๐งฎ **gcd is the denominator of every cost.**
2. ๐ฅ **A recursive function template is not unrollable โ overloads are.**
3. ๐ **Moving gcd out of the class template is what made Py construction a tie.**
4. ๐ **Match optimization flags** โ `-O3` vs `/O2`; clang-cl's `/O2` is cc1 `-O3`.
5. ๐งช **Fast paths must preserve NaN/โ/throw semantics.**
6. ๐งท **Don't "fix" a bug a test asserts without permission.**
7. ๐งฉ **Public API stability is a feature.**
$$ \text{speedup} \;=\; \frac{\text{Fraction}}{\text{PyFraction}},
\qquad {\color{#2e7d32}\text{add large (MSVC)}}:\;
\frac{35.92}{21.63} \approx {\color{#2e7d32}1.66\times} $$
> The best optimization is the work you can prove is redundant. โ
---
count: false
class: nord-dark, middle, center
## Q&A ๐ค
### GitHub: `luk036/fractions-cpp`
Slides: `projgeom/py-vs-ext-remark.html` ๐งฎ
---
count: false
class: nord-dark, middle, center
# ๐ Thank You
### Questions, benchmarks, or a PR? ๐