Pascal 程式在 ESP32 上是否比 C 程式慢?
Does Pascal code run slower than C code on the ESP32?
在 ESP32-S3 上比較 Pascal 與 C 執行浮點演算法的速度,Pascal 程式最初需時 3533.289 ms,是 C 程式 531.7 ms 的七倍。
Abstract. This article compares the runtime performance of a classical floating-point algorithm implemented in Pascal and in C on the ESP32-S3 microcontroller. The Pascal program initially ran seven times slower than the C program. Disassembly analysis attributes the difference to the absence of a hardware division instruction in the Xtensa FPU and to divergent strategies between the Free Pascal Compiler and GCC: the former uses software emulation (~240 cycles per division), the latter a reciprocal-based ROM routine (~40 cycles). A one-line declaration makes the ROM routine callable from Pascal, and together with two further optimizations the Pascal program ultimately outperforms the C program.
1. Introduction
Pascal is rarely used today, but active communities remain in niche domains. The ESP32 is a widely adopted microcontroller. The Free Pascal Compiler (FPC), the most popular open-source Pascal compiler, already includes support for the ESP-IDF SDK and the FreeRTOS operating system. How substantial is that support in practice?
2. Benchmark design
To answer this question, a classical floating-point algorithm — computing π with the Leibniz series — was implemented in Pascal and in C and executed on an ESP32-S3. A Pascal implementation of the algorithm was found in the speed-comparison repository on GitHub (leibniz.pas). Its loop body is:
for i := 2 to rounds + 1 do
begin
x := x * -1;
pi := pi + x / (2 * i - 1);
end;
The loop contains only floating-point multiplication and addition, occupies very little memory, and is compute-intensive — well suited to measuring compiler efficiency. The same repository provides a C implementation for comparison.
The FPU of the Xtensa core supports single precision only. All floating-point data was therefore changed to the Single type: FPC maps Real to Double, which would silently trigger software floating point. The C implementation was adapted accordingly — C floating-point literals such as 2.0 are double and would likewise invoke the software floating point routines of libgcc, so the literals were changed to 2.0f.
Both programs were then run on the ESP32-S3 (Xtensa LX7 dual core, 512 KiB of internal RAM) for one million iterations, using default compiler settings; for C, the default optimization level of ESP-IDF (-Og) was applied.
3. Results
Figure 1. Free Pascal implementation, one million iterations: 3533.289 ms.
Figure 2. C implementation (ESP-IDF, -Og), one million iterations: 531.7 ms.
The initial result is striking: the Pascal program took 3533 ms — seven times the runtime of the C program.
4. Root-cause analysis
The disassembly of both programs was compared (with the assistance of an AI assistant), which revealed an interesting fact: the Xtensa FPU has no division instruction. It provides 32-bit single-precision addition, subtraction, and multiplication — but not division.
In the absence of a hardware division instruction, Free Pascal calls its built-in software floating-point emulation routine float32_div, which costs several hundred instructions per division.
GCC, as shipped with ESP-IDF, instead calls a private routine located in the ESP ROM under the name __divsf3: thirty floating-point instructions that synthesize a division by computing a reciprocal, multiplying, and correcting the exponent. The AI assistant estimated software emulation at roughly 240 clock cycles per division, versus roughly 40 cycles for the reciprocal-based routine — one sixth of the cost.
5. Calling the ROM routine from Pascal
FPC does not implement the reciprocal method. Can Pascal code use __divsf3 anyway? It can: the symbol is always present in the ROM and only needs to be declared and called by name:
function __divsf3(a, b: single): single; cdecl; external name '__divsf3'; { declare __divsf3 for the Pascal program }
c := __divsf3(a, b);
This single declaration yields a six-fold speed-up, even though the compiler itself does not support the technique.
Two further improvements were suggested:
- FPC is not aggressive about mapping variables to registers. Global variables are re-loaded and stored (
lsi/ssi) on every access. Moving variables onto the subroutine stack as locals makes them eligible for register allocation. - One subexpression of the loop,
(2 * i - 1), is recomputed with shift and add instructions on every iteration; incrementing the previous value by 2 is sufficient and saves one instruction. The same technique applies to C.
The final loop body:
for i := 2 to rounds + 1 do
begin
x := -x; { neg.s — not mul.s by a -1.0 literal }
{ Avoid SoftFloat float32_div; call ROM __divsf3 like GCC. }
pi := pi + __divsf3(x, Single(denom));
Inc(denom, 2);
end;
6. Results after optimization
Figure 3. Optimized Pascal implementation, one million iterations: 525.477 ms.
The runtime improved seven-fold, and the Pascal program now outperforms the C program by 6 milliseconds.
7. Conclusion
As a compiled language, Pascal carries very little runtime overhead and belongs to the same class as C. The quality of the generated instruction stream, however, depends on the optimization effort of the compiler back end.
With respect to the current Xtensa back end of Free Pascal Compiler 3.3.1 (trunk), optimization clearly lags behind GCC — particularly given that GCC ran at a modest optimization level — in register allocation, sharing of intermediate values across statements, and loop optimization. This does not mean that Pascal is unsuitable today for MCUs such as the ESP series: the existing code quality is entirely adequate for many tasks, and where floating-point performance matters, compiler features that are not yet supported can be worked around at the application level.
來源:DEV Community · dev.to


