Hi all,
I want to share some benchmark results from the ongoing homogeneous-ravel
work in GNU APL 2.0. The short version: scalar comparison functions are
now 20–43× faster on large numeric arrays, and ⌈/⌊ are ~19× faster on
integer arrays. Note that the SVN numbers in the report below are my
local ones, not those in Savannah.
Background
----------
GNU APL 2.0 introduced "packed ravels": when an array has more than 12
elements and all cells are the same numeric type, the ravel is stored as
a flat C array of the native type (int64_t, double, etc.) rather than as
an array of 24-byte Cell objects. Six RavelType tags cover the cases:
*
RPT_CELLS — default, 24 bytes/element (Cell objects)
RPT_BOOL — 1 bit/element (bit-packed uint64_t words)
RPT_UNICODE16 — 2 bytes/element (UCS-2 characters)
RPT_UNICODE32 — 4 bytes/element (Unicode characters)
RPT_INT64 — 8 bytes/element (int64_t integers)
RPT_FLOAT64 — 8 bytes/element (double floats)
RPT_COMPLEX — 16 bytes/element (complex<double>)*
The previous release already had a FLOAT64→FLOAT64 fast path for + −
× ⌈ ⌊.
This round adds:
*
(a) INT64→INT64 fast path for ⌈ and ⌊ (max/min — overflow-safe)
(b) INT64→BOOL fast path for = ≠ < ≤ > ≥ (all six comparisons)
(c) FLOAT64→BOOL fast path for = ≠ < ≤ > ≥*
Note: + − × on INT64 are deliberately excluded because int64_t can
overflow; APL requires promotion to float in that case.
Test method
-----------
Platform: x86-64 Linux, single core
Compiler: GCC with -O2
Arrays: 1 000 000 elements, RPT_INT64 and RPT_FLOAT64 packing
confirmed
Timing uses the CPU cycle counter (*⎕FIO ¯1,* which calls *RDTSC*).
Each measurement is the minimum of three warmed runs, expressed as
cycles per element (lower is better).
Arrays were constructed so that packing actually fires:
AI_cell ← ⍳N ⍝ starts as RPT_CELLS (iota does not pack)
AI64 ← AI_cell + 0 ⍝ scalar + forces try_pack → RPT_INT64
AF64 ← AI_cell × 1.0
Comparison result arrays are *RPT_BOOL* (bit-packed output).
Results
-------*
Operation │ r3355 baseline │ r3357 current │ speedup
│ cycles/elem │ cycles/elem │
──────────────────────┼────────────────┼───────────────┼─────────
INT64 ⌈ (max) │ 20.4 │ 1.1 │ ~19×
INT64 ⌊ (min) │ 20.4 │ 1.1 │ ~19×
INT64 = │ 21.0 │ 0.5 │ ~42×
INT64 ≠ │ 21.0 │ 0.5 │ ~42×
INT64 < │ 21.0 │ 0.5 │ ~42×
INT64 ≤ │ 21.0 │ 0.5 │ ~42×
INT64 > │ 21.0 │ 0.5 │ ~42×
INT64 ≥ │ 21.0 │ 0.5 │ ~42×
──────────────────────┼────────────────┼───────────────┼─────────
FLOAT64 = │ 21.0 │ 0.5 │ ~42×
FLOAT64 ≠ │ 21.0 │ 0.5 │ ~42×
FLOAT64 < │ 21.0 │ 0.5 │ ~42×
FLOAT64 ≤ │ 21.0 │ 0.5 │ ~42×
FLOAT64 > │ 21.0 │ 0.5 │ ~42×
FLOAT64 ≥ (with ⎕CT) │ 21.5 │ 1.0 │ ~22×
──────────────────────┼────────────────┼───────────────┼─────────
INT64 + − × │ 21.0 │ 21.0 │ 1×
FLOAT64 + − × │ 1.1 │ 1.1 │ 1×
(FLOAT64 +−× fast path existed before r3355; INT64 excluded by
design)*
How the fast paths work
-----------------------
For each dyadic scalar function that supports a fast path, the subclass
overrides a virtual hook that returns a typed function pointer:
typedef ErrorCode (*vv_i2b_t)(uint64_t * Z,
const int64_t * A, const int64_t * B,
ShapeItem count);
The dispatch block in ScalarFunction::eval_VV() detects matching
RavelType tags, calls the hook, and if non-null runs the tight C++ loop
directly on the raw storage. For BOOL output the result is written as
bit-packed words and finalised with commit_ravel_Bool().
The ≥ comparison on *FLOAT64* uses ⎕CT (comparison tolerance), which
explains its slightly higher cost compared to the exact comparisons.
Planned: character fast path
-----------------------------
*RPT_UNICODE16* packs 4 characters per 64-bit word. An XOR-based approach
for = and ≠ should reduce flat-string comparison from the current
~80 cycles/element down to approximately 4 cycles/element (~20×).
That work is queued for the next round.
Best Regards,
Jürgen
committed to SVN, version now *2032*.