Skip to content

Commit e2a75fe

Browse files
committed
refactor(phase-2b): scalar pow and log follow-thru
1 parent 3e7eb7f commit e2a75fe

4 files changed

Lines changed: 320 additions & 43 deletions

File tree

docs/feature-work/2026-06-04-benchmark-hotspot-optimization-plan.md

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,8 @@ Result:
116116
- Benchmarked but rejected a direct `Cos` rewrite and an alternate `Asin`
117117
half-angle identity because the focused `ShortRun` measurements did not show
118118
a clear improvement.
119-
- Left `Pow`, `Log2`, and `Ln` unchanged in this pass. `Pow` remains a real
120-
scalar candidate, but it likely needs a dedicated `Log2`/`Pow2` algorithm pass
121-
with tighter correctness coverage rather than being mixed into the trig
122-
range-reduction work.
119+
- Left `Pow`, `Log2`, and `Ln` unchanged in this pass. The follow-up scalar
120+
pow/log pass was captured and completed as Phase 2b below.
123121

124122
Focused `ShortRun` evidence from this phase:
125123

@@ -144,6 +142,56 @@ dotnet build tests/FixedMathSharp.Benchmarks/FixedMathSharp.Benchmarks.csproj -c
144142
dotnet tests/FixedMathSharp.Benchmarks/bin/Release/net8.0/FixedMathSharp.Benchmarks.dll fixed64-arithmetic -j Short -i --exporters json
145143
```
146144

145+
## Phase 2b: Scalar Pow And Log Follow-Through
146+
147+
**Status:** Complete on 2026-06-05.
148+
149+
**Files:**
150+
151+
- Review: `src/FixedMathSharp/Core/FixedMath.Trigonometry.cs`
152+
- Review: `tests/FixedMathSharp.Tests/Core/FixedTrigonometry.Tests.cs`
153+
- Review: `tests/FixedMathSharp.Benchmarks/Fixed64ArithmeticBenchmarks.cs`
154+
155+
- [x] Capture focused baseline numbers for `Pow`, `Pow2`, `Log2`, and `Ln`.
156+
- [x] Add correctness sweeps for powers of two, fractional exponents, log
157+
identities, rounding behavior, and invalid inputs before runtime changes.
158+
- [x] Prefer removing repeated work and iteration count over lookup-table growth
159+
unless a table proves a clear cache-friendly win on `netstandard2.1`.
160+
- [x] Re-benchmark each changed method and record the before/after result.
161+
162+
Context: Phase 2 of the hotspot optimization plan improved trig-heavy scalar
163+
paths, but deliberately left `Pow`, `Pow2`, `Log2`, and `Ln` unchanged because
164+
they need a focused algorithm pass. `Pow` composes `Log2` and `Pow2`, so it is
165+
the clearest end-user hotspot, while `Ln` is a thin `Log2` derivative. This
166+
phase should keep the work measurement-driven and avoid changing approximation
167+
semantics unless tests prove the new raw results stay within accepted
168+
deterministic tolerances.
169+
170+
Phase 2b result on 2026-06-05: completed. `Pow2` now uses compact Q32.32
171+
fractional-bit lookup tables for positive and negative exponents instead of a
172+
division-heavy Taylor loop. `Log2` now normalizes by integer bit position instead
173+
of repeated shifting, and `Ln` no longer rounds the final result to an integer.
174+
This also fixed two correctness issues discovered during the phase:
175+
`Pow2(31)` wrapped negative instead of saturating, and `Ln` discarded fractional
176+
log results such as `ln(0.125)`.
177+
178+
Focused `ShortRun` evidence:
179+
180+
| Benchmark | Before | After |
181+
| --- | ---: | ---: |
182+
| `Pow` | 118.184 us, 0 B | 61.974 us, 0 B |
183+
| `Pow2` | 75.779 us, 0 B | 9.349 us, 0 B |
184+
| `Log2` | 33.442 us, 0 B | 31.224 us, 0 B |
185+
| `Ln` | 41.089 us, 0 B | 33.225 us, 0 B |
186+
187+
Verification:
188+
189+
```bash
190+
dotnet test tests/FixedMathSharp.Tests/FixedMathSharp.Tests.csproj --configuration Debug --no-restore --filter "FullyQualifiedName~FixedTrigonometry"
191+
dotnet build tests/FixedMathSharp.Benchmarks/FixedMathSharp.Benchmarks.csproj -c Release -f net8.0 --no-restore
192+
dotnet tests/FixedMathSharp.Benchmarks/bin/Release/net8.0/FixedMathSharp.Benchmarks.dll fixed64-arithmetic -j Short -i --filter "*Pow*" "*Log2*" "*Ln*" --exporters json
193+
```
194+
147195
## Phase 3: Quaternion Direction And Euler Paths
148196

149197
**Why:** Quaternion conversion-heavy paths are among the slowest allocation-free

src/FixedMathSharp/Core/FixedMath.Trigonometry.cs

Lines changed: 164 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,78 @@ public static partial class FixedMath
6767
internal const double SIN_COEFF_7_DOUBLE = 0.00019588856957852840423583984375d; // 1/7!
6868
internal static readonly long SIN_COEFF_7_LONG = (long)(SIN_COEFF_7_DOUBLE * ONE_L);
6969

70+
private static readonly long[] s_pow2PositiveFractionLookup =
71+
{
72+
6074001000L,
73+
5107605667L,
74+
4683695048L,
75+
4485121744L,
76+
4389014833L,
77+
4341736423L,
78+
4318288544L,
79+
4306612134L,
80+
4300785774L,
81+
4297875550L,
82+
4296421177L,
83+
4295694175L,
84+
4295330720L,
85+
4295149004L,
86+
4295058149L,
87+
4295012722L,
88+
4294990009L,
89+
4294978653L,
90+
4294972974L,
91+
4294970135L,
92+
4294968716L,
93+
4294968006L,
94+
4294967651L,
95+
4294967473L,
96+
4294967385L,
97+
4294967340L,
98+
4294967318L,
99+
4294967307L,
100+
4294967302L,
101+
4294967299L,
102+
4294967297L,
103+
4294967297L
104+
};
105+
106+
private static readonly long[] s_pow2NegativeFractionLookup =
107+
{
108+
3037000500L,
109+
3611622603L,
110+
3938502376L,
111+
4112874773L,
112+
4202935003L,
113+
4248701965L,
114+
4271771996L,
115+
4283353945L,
116+
4289156690L,
117+
4292061010L,
118+
4293513907L,
119+
4294240540L,
120+
4294603903L,
121+
4294785595L,
122+
4294876445L,
123+
4294921870L,
124+
4294944583L,
125+
4294955939L,
126+
4294961618L,
127+
4294964457L,
128+
4294965876L,
129+
4294966586L,
130+
4294966941L,
131+
4294967119L,
132+
4294967207L,
133+
4294967252L,
134+
4294967274L,
135+
4294967285L,
136+
4294967290L,
137+
4294967293L,
138+
4294967295L,
139+
4294967295L
140+
};
141+
70142
#endregion
71143

72144
#region FixedTrigonometry Operations
@@ -110,40 +182,34 @@ public static Fixed64 Pow2(Fixed64 x)
110182
if (x.m_rawValue == 0)
111183
return Fixed64.One;
112184

113-
// Handle negative expFixed64.Onents by using the reciprocal
114185
bool neg = x.m_rawValue < 0;
115186
if (neg)
116187
x = -x;
117188

118189
if (x == Fixed64.One)
119190
return neg ? Fixed64.One / Fixed64.Two : Fixed64.Two;
120191

121-
if (x >= Fixed64.Log2Max)
122-
return neg ? Fixed64.One / new Fixed64(MAX_VALUE_L) : new Fixed64(MAX_VALUE_L);
123-
124-
/*
125-
* Taylor series expansion for exp(x)
126-
* From term n, we get term n+1 by multiplying with x/n.
127-
* When the sum term drops to Fixed64.Zero, we can stop summing.
128-
*/
129-
int integerPart = Fixed64.RawToInt(Floor(x));
130-
x = Fixed64.FromRaw(x.m_rawValue & MAX_SHIFTED_AMOUNT_UI); // Fractional part
131-
132-
var result = Fixed64.One;
133-
var term = Fixed64.One;
134-
int i = 1;
135-
while (term.m_rawValue != 0)
192+
int integerPart = (int)(x.m_rawValue >> SHIFT_AMOUNT_I);
193+
long fractionalRaw = x.m_rawValue & MAX_SHIFTED_AMOUNT_UI;
194+
195+
if (neg)
136196
{
137-
term = FastMul(FastMul(x, term), Fixed64.Ln2) / (Fixed64)i;
138-
result += term;
139-
i++;
197+
if (integerPart >= SHIFT_AMOUNT_I)
198+
return Fixed64.MinIncrement;
199+
200+
Fixed64 result = Pow2Fractional(fractionalRaw, s_pow2NegativeFractionLookup);
201+
return Fixed64.FromRaw(ShiftRightRounded(result.m_rawValue, integerPart));
140202
}
141203

142-
result = Fixed64.FromRaw(result.m_rawValue << integerPart);
143-
if (neg)
144-
result = Fixed64.One / result;
204+
if (integerPart >= 31)
205+
return Fixed64.MaxValue;
145206

146-
return result;
207+
Fixed64 positiveResult = Pow2Fractional(fractionalRaw, s_pow2PositiveFractionLookup);
208+
long shifted = positiveResult.m_rawValue << integerPart;
209+
210+
return shifted < 0
211+
? Fixed64.MaxValue
212+
: Fixed64.FromRaw(shifted);
147213
}
148214

149215
/// <summary>
@@ -160,21 +226,14 @@ public static Fixed64 Log2(Fixed64 x)
160226
throw new ArgumentOutOfRangeException(nameof(x), "Cannot compute logarithm of non-positive number.");
161227

162228
long b = 1U << (SHIFT_AMOUNT_I - 1); // Initial value for binary logarithm
163-
long y = 0; // Result accumulator
164229
long rawX = x.m_rawValue;
230+
int shift = FloorLog2((ulong)rawX) - SHIFT_AMOUNT_I;
231+
long y = (long)shift << SHIFT_AMOUNT_I;
165232

166-
// Adjust rawX to the correct range [1, 2)
167-
while (rawX < ONE_L)
168-
{
169-
rawX <<= 1;
170-
y -= ONE_L;
171-
}
172-
173-
while (rawX >= (ONE_L << 1))
174-
{
175-
rawX >>= 1;
176-
y += ONE_L;
177-
}
233+
if (shift > 0)
234+
rawX >>= shift;
235+
else if (shift < 0)
236+
rawX <<= -shift;
178237

179238
Fixed64 z = Fixed64.FromRaw(rawX); // Remaining fraction
180239

@@ -201,7 +260,75 @@ public static Fixed64 Ln(Fixed64 x)
201260
if (x.m_rawValue <= 0)
202261
throw new ArgumentOutOfRangeException(nameof(x), "Cannot compute logarithm of non-positive number.");
203262

204-
return FastMul(Log2(x), Fixed64.Ln2).Round();
263+
return FastMul(Log2(x), Fixed64.Ln2);
264+
}
265+
266+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
267+
private static Fixed64 Pow2Fractional(long fractionalRaw, long[] lookup)
268+
{
269+
Fixed64 result = Fixed64.One;
270+
long mask = 1L << (SHIFT_AMOUNT_I - 1);
271+
272+
for (int i = 0; i < SHIFT_AMOUNT_I; i++)
273+
{
274+
if ((fractionalRaw & mask) != 0)
275+
result = FastMul(result, Fixed64.FromRaw(lookup[i]));
276+
277+
mask >>= 1;
278+
}
279+
280+
return result;
281+
}
282+
283+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
284+
private static long ShiftRightRounded(long value, int shift)
285+
{
286+
if (shift == 0)
287+
return value;
288+
289+
long half = 1L << (shift - 1);
290+
return (value + half) >> shift;
291+
}
292+
293+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
294+
private static int FloorLog2(ulong value)
295+
{
296+
int result = 0;
297+
298+
if (value >= 1UL << 32)
299+
{
300+
value >>= 32;
301+
result = 32;
302+
}
303+
304+
if (value >= 1UL << 16)
305+
{
306+
value >>= 16;
307+
result += 16;
308+
}
309+
310+
if (value >= 1UL << 8)
311+
{
312+
value >>= 8;
313+
result += 8;
314+
}
315+
316+
if (value >= 1UL << 4)
317+
{
318+
value >>= 4;
319+
result += 4;
320+
}
321+
322+
if (value >= 1UL << 2)
323+
{
324+
value >>= 2;
325+
result += 2;
326+
}
327+
328+
if (value >= 1UL << 1)
329+
result++;
330+
331+
return result;
205332
}
206333

207334
/// <summary>

tests/FixedMathSharp.Benchmarks/Fixed64ArithmeticBenchmarks.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ public Fixed64 Pow()
122122
return accumulator;
123123
}
124124

125+
[Benchmark]
126+
public Fixed64 Pow2()
127+
{
128+
Fixed64 accumulator = Fixed64.Zero;
129+
for (int i = 0; i < _unit.Length; i++)
130+
accumulator += FixedMath.Pow2(_unit[i]);
131+
132+
return accumulator;
133+
}
134+
125135
[Benchmark]
126136
public Fixed64 Log2()
127137
{

0 commit comments

Comments
 (0)