> contributed by < `RealBigMickey` >
## TL;DR 相關連結:
**$e^x - 1$ no libc:**[ Github 連結](https://github.com/RealBigMickey/Linux2025/blob/main/exp_without_FPU.c)
**$e^x - 1$ no FPU& libc:**[Github 連結](https://github.com/RealBigMickey/Linux2025/blob/main/expm1_signedmag_noFPU_nolibc.c)
## Q1: switch_to 巨集的作用?保存/還原 CPU context。為何參數列表中,有 “last”? 其考量因素是什麼?
`switch_to(prev, next, last);`
找資料時翻到一篇 [26 年前的論壇](https://nlo.lists.kernelnewbies.narkive.com/fF2lWigo/why-does-switch-to-need-three-arguments)講道這個疑問:
樓主 Bao zhao 問說 last 的用處是甚麼, prev 與 last 不是一樣嗎?我知道 last 不一定是 prev ,因排成器可能有 preemption 或其他插隊行為,不過 last 的應用仍不是很清楚。
底下 Jon Masters 回應如下:
:::spoiler
Page 92 of Understanding the Linux Kernel states:
``You might easily guess the role of prev and next - they are just
placeholders for the local variables prev and next - but what about
the third parameter last? Well, the point is that in any process
switch, three processes are involved, not just two.
Suppose the kernel decides to switch off process A and to activate
process B. In the schedule() function, prev points to A's descriptor
and next points to B's descriptor. As soon as the switch_to macro
deactivates A, the execution flow of A freezes.
Later, when the kernel wants to reactivate A, it must switch off
another process C (in general, this is different from B) by executing
another switch_to macro with prev pointing to C and next pointing to
A. When A resumes its execution blow, it finds its old kernel mode
stack, so the prev local variable points to A's descriptor and next
points to B's descriptor. The kernel, which is now executing on behalf
of process A, has lost any reference to C.''
[ Quoted text from Understanding the Linux kernel, second edition.
O'relly and associates. ISBN: 0-596-00213-0. RRP 49.95USD, 77.95CAN.
http://www.oreilly.com/ ]
So what it's getting at is that during context switch, the kernel
stack and current process switches so the local variables available
within the function doing the switch also change. This needs to be
accounted for ahead of time by providing the kernel with enough
information to deal with switching processes back at a later point in
time.
...
:::
簡單來說,Jon Masters 提到 "Understanding the Linux Kernel" 一書中有解釋:任何 process switch 中至少會涉及三個程序,而非兩個。
假設有三程序 A B 與 C ,A 的下一個程序為 B,B 下一個為 C ,而 C 為 A 。當執行緒回到 A 時,將失去 C 的資訊 (lose reference)。
### Lose reference 會怎樣嗎?
Linux 核心在需要上一個程序的理由:
- 觀察程序執行狀況(i.e. CPU 占用率、timeslice、是否被 preempted 等)
- 準確紀錄程序執行史
- 排成器更新程序資訊、快速決定 priority
```c
// e.g.
if (task_just_ran(last) && should_resume_soon(last))
boost_priority_or_skip_queue(last);
```
- Cache-aware Scheduling
- 原先的程序或許 cache 還沒被清掉,last 讓排成器能快速回到上個程序,少了它增加 cache miss 的可能性
## Q2: 哪裡會用到 e^x - 1 (提示: 課程教材有)
⇒ https://man7.org/linux/man-pages/man3/expm1.3.html
問答中被問道`float expm1f(float x);` 函式,若已經有函式快速計算 e^x ,那還有必要有 e^x - 1 的函式嗎?Linux 核心很少會有多餘的程式,會這樣設計就代表 e^x - 1 夠常出現,有那個價值。
首先得先了解,C lib 中是怎麼計算浮點數指數的。
### 計算浮點數指數:
首先先排除&處理所有特例:
```c
if (isnan(x)) return x; // exp(NaN) → NaN
if (x == +Inf) return +Inf; // exp(+∞) → +∞
if (x == -Inf) return 0.0; // exp(−∞) → 0
x > 709.782712893384 → return +HUGE_VAL; // e.g. exp(710) overflows
x < -745.1332191019411 → return 0.0; // exp(−745) underflows
```
在 C lib 等典型函式庫中,會先將數字拆成兩部份,一大一小。分成兩部份有幾個優點,第一是 2^k 可以用浮點數完整表示(沒有誤差),而用浮點數表示小數字 r 也遠比直接拿 x 計算來得準確,二是善用 bitops 與二進位的特性,執行速度上比較快:
```c
exp(x) = exp(k * ln2 + r) = exp(k * ln2) * exp(r) = 2^k * exp(r)
```
k 只須使用 [bitops](https://hackmd.io/@sysprog/c-bitwise) (clz等) 即可快速求出,而 r 再從其結果求出:
```c
// e.g.
const double ln2 = 0.6931471805599453094172;
const double inv_ln2 = 1.4426950408889634073599; // 1/ln2
...
k = round(x * inv_ln2) // x/ln2, rounding ties to even
r = x - k * ln2
```
兩鄉進浮點數進行減法時容易失去精度,在 x 接近 k * LN2 時會遇到,因此會將 ln2 分成兩部份來保留數字精確度:
```c
const double ln2_hi = 0x1.62E42FEFA39EF; // ≈ 0.6931471805599453
const double ln2_lo = 0x1.3691B8A2E037; // ≈ 1.9082149292705877e-10
r = x - k * ln2_hi;
r = r - k * ln2_lo;
```
指數的定義 → 一泰勒展開式:
```clike
exp(x) = 1 + x + x^2/2! + x^3/3! + ... (to n where n -> ∞)
```
這樣算到極限是不可能的;取大 n 時計算耗時長,只取固定幾項的話隨著 x 遠離 0 ,精度會迅速下降,利用泰勒無法找到一適用於任何數的公式。
:::success
解:利用多項式
```clike
exp(r) ≈ 1 + r + r^2*P1 + r^3*P2 + r^4*P3 + ... + r^N*PN
```
:::
已知 r 的區間為 `|r| ≤ 0.5 * ln2 ≈ 0.34657359028`
```c
/* Coefficients for polynomial approximation of exp(r) in [-0.34658, 0.34658] */
static const double
P1 = 1.66666666666666019037e-01, /* 0x3FC555555555553E */
P2 = -2.77777777770155933842e-03, /* 0xBF66C16C16BEBD93 */
P3 = 6.61375632143793436117e-05, /* 0x3F11566AAF25DE2C */
P4 = -1.65339022054652515390e-06, /* 0xBEBBBD41C5D26BF1 */
P5 = 4.13813679705723846039e-08; /* 0x3E66376972BEA4D0 */
```
一項一項乘開也可以,不過利用 [霍納算法(Horner's method)](https://ohiyooo2.pixnet.net/blog/post/403086710) 可少用幾次乘法(次數:20 -> 6):
```clike
exp(r) ~= 1 + r + r^2*P1 + r^3*P2 + r^4*P3 + r^5*P4 + r^6*P5
// Horner's method
= 1 + r*(1 + r*(P1 + r*(P2 + r*(P3 + r*(P4 + r*P5)))));
```
最後結果會是 `result = y * 2^k` ,浮點數乘上 2 的倍數可透過 bitshift exponent 來快速求出結果
**[C lib 的原始程式碼](https://github.com/bminor/glibc/blob/master/sysdeps/ieee754/dbl-64/e_exp.c)**
注意 exp(r\),前面的 1 去掉即是 expm1(),用 exp() - 1 雖然需要稍微多花點時間去計算,但若真的只是 + 1 後 - 1,計算量可以說非常渺小,應該沒必要額外創個新函式。
### 何時會用到 expm1() ?
根據 IEEE 754 標準,浮點數有分 normalized number 跟非 normalized number 兩種狀態,< 2⁻¹²⁶ 時
假設一情況, x 為一接近 0 、非常小的數字,則 exp(x) 會因為浮點數的經度問題而簡略成 1.0 ,此時若減去 1 結果只剩下 0 。
道理都懂,但具體多小這才會成立?
```c
float x = 1.0f;
for(int i = 0; i < 7; i++) {
printf("Num: %.20f\n", x);
int temp = *(int*)&x + 1;
x = *(float*)&temp;
}
stdout:
Num: 1.00000000000000000000 // A
Num: 1.00000011920928955078 // B
Num: 1.00000023841857910156
Num: 1.00000035762786865234
Num: 1.00000047683715820313
Num: 1.00000059604644775391
Num: 1.00000071525573730469
```
若 `exp(x) < 0.5 * (B - A)`的值,則結果會被省略為 1.0,即使 > 也會因為浮點數的精度問題而無法精確表示數字的值。因此若希望計算較準確的指數,則需用額外的函式,在裡面就考慮到 -1 的部分。
### 那為甚麼沒有 m2 m3 m4 等?
確實,在 x 趨近 ln2 等時候, exp(x) - ln2 也會出現精度問題而失去資訊,結果為 0。但這情況並不常見,`exp(x) - 1`常見是因為不管是在電腦科學、數學、還是機率或統計學中,許許多多公式、算式的答案會趨近於 0 ,其指數容易不準確。但接近ln2、ln3 等數字,通常主軸並不是在小數點後面的數字而是在前面,後面幾位數字對整體數字而言影響並不大。
若真的有此需求,再自己寫就可以了 :D
## 補充:在 IEEE 754 單精度浮點數,不使用 libc 的情況下計算 e^x & e^x - 1
*`(留意誤差計算及控制)`*
### my_exp():
先列出例外,回顧程式設計一的內容,+inf 的 bit pattern = 0x7F800000,-inf = 0xFF800000 ,再設計小實驗找 breaking point:
```c
for(float i = 0; i < 100; i += 0.1) {
printf("Num(%.1f): %f\n", i , expf(i));
}
// stdout:
...
Num(88.7): 332338964023764323258539905938112905216.000000
Num(88.8): inf
...
```
```c
for(float i = 0; i > -200; i -= 0.1) {
printf("Num(%.1f): %.20f\n", i , expf(i));
if(expf(i) == 0.0f)
return 0;
}
// last line printed: Num(-104.0): 0.00000000000000000000
```
還有個情況,當 x 為 NaN (Not a Number)。可以用 math.h 函式庫的 `isnan()`,但 Linux 核心中不能使用 c library,可以的話就不用。根據 [IEEE 754 的說明書](https://www.gnu.org/software/libc/manual/html_node/Infinity-and-NaN.html) 上的定義: `NaN is unordered: it is not equal to, greater than, or less than anything, including itself. x == x is false if the value of x is NaN.`
因此可以用 `if (x != x)` 來判斷 NaN
如 Q2 所述,利用泰勒展開時 x 一旦遠離 0 ,若項式不夠則誤差會迅速累積,項式多又得花許多時間。
:::success
**Solution** -> Minimax polynomial approximation
| 常數 | 數值 |
| --- | --- |
| P1 | 0.9999280752600668 |
| P2 | 1.0001641903948264 |
| P3 | 0.5049632650961922 |
| P4 | 0.1656683995499798 |
:::
搭配上述 Horner's Method ,最後計算出 `result = y * 2^k`。這邊寫一 helper function 來輔助計算,雖然較長但邏輯上相對 expm1 簡單些:
```c
float my_ldexpf(float x, int exp) {
if (x == 0.0f) {
return x;
}
union { float f; int i; } u = { .f = x };
int old_exp = (u.i >> 23) & 0xff; // extract old expo
if(old_exp == 0) {
// could be de-normallized num
while(has_mantissa(u.i) && !has_exponent(u.i)) {
u.i <<= 1;
exp--;
}
}
exp = exp + old_exp;
if (exp <= 0) {
return 0.0f;
} else if (exp >= 255) {
u.i = (u.i & 0x80000000) | 0x7f800000;
return u.f;
}
u.i &= ~0x7f800000;
u.i |= (exp << 23) & 0x7f800000;
return u.f;
}
```
善用 union 避免一直寫 `*(int*)&` 先判斷是否 de-normallized
-> 是:
- 利用 while 迴圈左移直到沒 mantissa 或是有 exponent
- 若移完
-> 否:
若只是利用巨集寫個簡單的四捨五入很簡單 e.g.
```c
#define FAST_ROUND(x) ((x >= 0.0f) ? (int)(x + 0.5f) : (int)(x - 0.5f))
```
但根據 [IEEE 754 浮點數運算標準](http://www.dsc.ufcg.edu.br/~cnum/modulos/Modulo2/IEEE754_2008.pdf) 的 4.3.1 章 Rounding-direction attributes to nearest,在進行 Rounding 時應 "Round to nearest even" 取最近的偶數整數。
每次都無條件進位或無條件捨去,經過大量運算後,這些誤差會往同一個方向累積,產生系統性的偏差。Round to nearest even 平均分配進位與捨去的次數,使得整體誤差在大量運算後能互相抵消減少偏差。
因此一關鍵是如何實現有效率的 ["Round to nearest even"](https://lemire.me/blog/2020/04/16/rounding-integers-to-even-efficiently/) (四捨六入五成雙)
```c
static inline int round_to_nearest_even(float x) {
int i = (int)x;
float frac = x - (float)i;
if (frac > 0.5f || (frac == 0.5f && (i & 1)))
return i + 1;
else if (frac < -0.5f || (frac == -0.5f && (i & 1)))
return i - 1;
else
return i;
}
```
把整數與小數部分分開,再去判斷如何對齊。若小數部分 == 0.5 則利用 `i & 1` 判斷是否為偶數。
但這有個問題:

這是 e^x 的曲線,每個 y = X.5 只有一個對應的 x 座標,一任意 x 值算出的結果剛好為 X.5 的情況發生的機率可說是非常渺茫,只要略為有偏差就不會成立。
```c
static inline int round_to_nearest_even(float x) {
int i = (int)x;
float frac = x - (float)i;
if (frac > 0.5f || (frac == 0.5f && (i & 1)))
return i + 1;
else if (frac < -0.5f || (frac == -0.5f && (i & 1)))
return i - 1;
else
return i;
}
```
### my_expm1():
前面已經提過使用 `expm1(x)` 通常會在 x 趨近 0 時,而泰勒展開式越是接近 0 越準確,而且第一項剛好是 1。當然再次使用 Minimax approximation 也是可以,但要注意重新找各常數,精準度或許也遜於泰勒。
最後我決定取泰勒展開的前 6 項:
```c
float y = x * (1.0f + x*(0.5f + x*(1.0f/6.0f + x*(1.0f/24.0f + x*(1.0f/120.0f)))));
```
實驗輸出如下:
```java
// stdout:
x my_expm1(x) expm1f(x) my_exp(x)-1 RelError (%)
-------------------------------------------------------------------------------------------------
0.000000 0.00000000 0.00000000 -0.00007194 0.000000
0.000001 0.00000100 0.00000100 -0.00007093 0.000000
0.350000 0.41906488 0.41906759 0.41898012 0.000647
0.500000 0.64869791 0.64872122 0.64878702 0.003593
(ln2)0.693147 0.99982929 1.00000000 0.99985611 0.017071
0.750000 1.11672354 1.11700010 1.11690569 0.024760
1.000000 1.71666670 1.71828175 1.71832895 0.093992
5.000000 90.41667938 147.41316223 147.41792297 38.664444
10.000000 1476.66650391 22025.46484375 22026.21875000 93.295639
20.000000 34886.67187500 485165184.00000000 485142976.00000000 99.992813
```
可以看到在 0.5 時 `my_expm1(x)` 仍略準於 `my_exp(x)-1` ,但到了 ln2 時則略遜於(非常渺小的差距),因此設定當 x > ln2 或 x < -ln2 時用 `my_exp(x)-1` 來計算
**Benchmark Results:**
```c
float my_expm1(float x) {
if (x != x) // check if NaN
return x;
if (x == P_INF)
return P_INF;
if (x == N_INF)
return -1.0f;
if (x >= 88.8)
return P_INF;
if (x <=-104)
return -1.0f;
if (x > ln2_f || x < neg_ln2_f)
return my_exp(x) - 1;
// Taylor's series
float y = x * (1.0f + x*(0.5f + x*(1.0f/6.0f + x*(1.0f/24.0f + x*(1.0f/120.0f)))));
return y;
}
```



`my_expm1(x)` 在 x < ln2 也就是使用泰勒展開計算時,其實執行速度比 `expm1f(x)` 快了不少,但一旦開始使用 Minimax 來估計速度上會慢 `expm1f(x)` 一截。不過光是有些時候比 C lib 函式庫快就很奇妙了。
**[my_exp(x), my_expm1(x) without libc -> Github 程式碼](https://github.com/RealBigMickey/Linux2025/blob/main/exp_without_FPU.c)**
## Q3: 在 IEEE 754 單精度浮點數,不使用 FPU 的情況下計算 e^x & e^x - 1
*`(留意誤差計算及控制)`*
回顧 [2025 第五週測驗一](https://hackmd.io/@sysprog/linux2025-quiz5) 當時教授提到 Linux 核心之中禁止使用 FPU 的問題,並提到利用定點數來表示小數這解法,例子中使用 32 位元的定點數。
這邊我也選擇使用 32 位元的定點數,不過 **不是使用 2's complement 而是 Sign-magnitude**來表示數值。位元配置如下:
- 1 sign bit、15 位元表示整數、16 位元表示小數,但具體比例可以根據需求自行調整。

- 可表示的最大整數範圍為 $\pm 32767$ ,最小小數精度為 $\pm 1/65538$ $\approx \pm 0.00001525832$
- 定義為:
```c
typedef uint32_t fix16_t;
```
測驗題中與這題最有關係的函式 fix16_exp,利用定點數用 for 迴圈去計算泰勒展開式,直到結果夠準確離開迴圈。那麼若要計算 expm1,則把泰勒展開的第一項也就是 FIX16_ONE 刪掉。
不過實際上還有其他因素得去考慮,下面會詳細講道。
這題的問題的關鍵之一便是 *「如何不使用 FPU 將 float 轉為 fix_16 ,再轉回來」*
### float 轉為 fix16:
```c
static inline fix16_t float_to_fix16(float a)
{
if(a != a) // check if NaN
return 0;
if (a == INFINITY)
return FIX16_PINF;
if (a == -INFINITY)
return FIX16_NINF;
union { float f; int32_t i; } u = { .f = a };
int32_t sign = u.i & 0x80000000;
int32_t exp = ((u.i & 0x7f800000) >> 23) - 127;
int32_t mantissa = u.i & 0x007fffff | 0x00800000;
if (exp > 15)
return FIX16_PINF;
if (exp < -15)
return 0;
mantissa >>= 7;
if (exp >= 0)
mantissa <<= exp;
else
mantissa >>= -1*exp;
return (mantissa | sign);
}
```
邏輯上不難,浮點的定義就是二進位的科學記號。
當 Exponent 為 1時,浮點數即為 Fraction 有 23 位元的 fix16。因此只要根據 Exponent 的大小對 Mantissa 做對應的位移、補上浮點數中省略的 1.X * 2^expo 中的 1,就能順利轉換。
### fix16 轉為 float:
```c
static inline float fix16_to_float(fix16_t a)
{
int32_t result_f = a & 0x80000000; // get sign bit
a &= 0x7fffffff;
int32_t exp = 15 - clz32(a);
if (exp >= 0) {
a >>= exp;
} else {
a <<= -1*exp;
}
int32_t mantissa = a & 0x0000ffff;
result_f |= (exp + 127) << 23;
result_f |= mantissa << 7;
union { uint32_t u; float f; } conv = { .u = (uint32_t)result_f };
return conv.f;
}
```
回想起第二週測驗 2 的內容,clz()`(count leading zeros)` 利用遞迴將 32 位元的 int 分割為 2 位元的部分,再根據數值為 [0, 1, 2, 3] 等回傳 [2, 1, 0 ,0] 來計算出 MSB 數過來 0 的數量。
這邊 leading zero 的數量與 Exponent 息息相關,15 - clz(a) 算出第一個非 0 位元的準確位置,+127 的 bias 後即為 Exponent 的值。
將第一個非 0 位元位移到 MSB 數過來第 16 個位元,後面 16 位元即為 Mantissa,將 sign、exponent、mantissa 放到對應的位置後,就是 fix16 轉換後的浮點數
::: info
當然因為數值的表示方式,這之間的轉換會失去一些資訊。太小或太大的數字無法用 fix16 表示,中間的數字也可能會微微偏離原本的數值,但整體而言大部分正常情況下不會結果不會與 expm1() 偏離太多。
:::
### fix16_expm1():
首先是抓極限
- $e^{12.6995} \approx 327584.0688845356$
- $e^{-11.09} \approx 0.0000152642052$
```c
#define FIX16_ONE 0x00010000U /* +1.0 */
#define FIX16_e_1 0x0002B7E1U /* e^1 ≈ 2.7182 */
#define FIX16_PINF 0x7FFFFFFFU
#define FIX16_NINF 0xFFFFFFFFU
#define FIX16_exp_PMAX 0x000CB310U // ≈ 12.699463
#define FIX16_exp_NMAX 0x800b1708U // ≈ -11.089966
```

*因為 float_to_fix16 函式的設計,正常情況下不會有 -0 的狀況出現,最後選擇省略列出此例外。*
```c
if (in == 0)
return 0;
if (in == FIX16_ONE)
return FIX16_e_1 - FIX16_ONE;
if (in >= FIX16_exp_NMAX)
return (FIX16_ONE | 0x80000000); // return -1
if (in >= FIX16_exp_PMAX && in < 0x80000000)
return FIX16_PINF;
```
再來處理 sign bit 、善用 neg 只能是 0x80000000 或 0x00000000 的特性,利用一次 xor 處理數值:
```c
int neg = in & 0x80000000;
if (neg)
in ^= neg; // set MSB to 0
```
同於原測驗題與 "不用 libc" 的實作,使用泰勒展開估計,展開直到 term 夠小:
```c
fix16_t result = in;
fix16_t term = in;
fix16_t term = in;
for (int i = 2; i < EXP_SERIES_MAX_TERMS; i++) {
term = fix16_mul(term, fix16_div(in, int_to_fix16(i)));
result += term;
/* Break early if the term is sufficiently small */
if ((term < EXP_TERM_SMALL_THRESHOLD) &&
((i > EXP_TERM_MIN_ITER) || (EXP_TERM_TINY_THRESHOLD < 20)))
break;
}
```
最後處理in 為負數的情況。
- **注意**:因為 signed magnitude ,得特別關注 unsigned 與 signed 之間的減法運算,確保不會有負數結果而出錯:
```c
if (neg) {
result = FIX16_ONE - fix16_div(FIX16_ONE, result + FIX16_ONE);
result |= neg;
}
return result;
}
```
**[my_expm1(x) without FPU、libc -> Github 程式碼](https://github.com/RealBigMickey/Linux2025/blob/main/expm1_signedmag_noFPU_nolibc.c)**
## Q4: 何時會用到雙曲函數
⇒ https://en.wikipedia.org/wiki/Hyperbolic_functions
tanh(x) is defined as (exp(x) - exp(-x)) / (exp(x) + exp(-x))
我自己第一個想到的就是訊號處理,準確一點是傅立葉轉換,大家常說把訊號分解成 sin 跟 cos 的組合,不過其實在某些微分方程的解裡,像是傳輸線、熱傳導問題等,都會有用到雙曲函數的場景。
利用程式接計算 tanh(x) 耗時長、運算量龐大,因此多數場景會選擇好比 Q3 那樣去估計,泰勒展開在 $x=0$ 展開時,前幾項為:
$$
\tanh(x) = x - \frac{x^3}{3} + \frac{2x^5}{15} - \frac{17x^7}{315} + \cdots
$$
當 $x$ 很小的時候,**只用前幾項多項式**來近似 tanh(x) 已經很準。例如:
$$
\tanh(x) \approx x - \frac{x^3}{3}
$$
---
一對一時被詢問此問題時,我給的答案是「tanh 可以拿來逼近其他函數」,但當下想不到一個現實中的應用場景。實際上 tanh 乃至雙曲函數確實會拿來「逼近」,逼近或應用函數常見的特性為「非線性轉折」或「階梯/符號函數」。
- **符號函數 sign(x):**
$$
\text{sign}(x) \approx \tanh(kx)
$$
當 $k$ 很大時,tanh(kx) 就會變得很陡峭,接近 sign(x) 的 1/-1 跳變,但又不會有數學上的不連續。常用於神經網路之中。
- **單位階梯函數 step(x):**
$$
\text{step}(x) \approx 0.5 \times (1 + \tanh(kx))
$$
把階梯變成一條 S 曲線,k 越大會越接近理想階梯。
---
最後看一下數學上定義:
$$
\sinh(x) = \frac{e^x - e^{-x}}{2}
$$
$$
\cosh(x) = \frac{e^x + e^{-x}}{2}
$$
$$
\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
$$
因此只要有 exp() 或是 雙曲函數 兩者之一,變可只使用加法器 + 一次乘法就能求出另一者。
---
> May 31, 2025