Use __int128 for generic umul_ppmm and smul_ppmm - #2820
Conversation
|
Yeah, I think this is good. Do you have any benchmark on x86 comparing inline assembly versus this short 128-bit arithmetic? As Fredrik mentioned here, it was slower on older GCC, but GCC has improved on this area then I'd like to remove the inline assembly. |
|
I did not have x86 numbers, so I measured it (with my local infrastucture) -- on every x86 box I could get my hands on: Zen 2 (Ryzen 3800XT), Raptor Lake (i7-13700KF), Haswell (i5-4590S) and Sandy Bridge-EP (Xeon E5-2650, over proxmox), across gcc 12.2 / 14.2 / 15.2 / 16.2. Method: default Codegen first: for these two macros gcc already emits exactly what the asm does -- a single End to end,
So: on 2012-2014 cores the two forms trade blows within ~3% either way; on anything recent the Caveats: the Raptor Lake numbers are from a Linux environment under WSL2 and the Xeon is a live virtualization host (its first warm-up run was discarded) -- but every comparison is A/B inside the same environment, so the deltas hold. And this only covers Sorry for the 6 hours waiting it took me a while to run and configure the environments. |
|
Sounds great! Can you remove the inline assembly definitions of |
|
For the generic fallback code this is certainly fine, but there are indeed several places where I've measured a slowdown on x86-64. A recently commented example is We'd have to recheck whether this applies when the |
|
I would guess that |
|
OK, my up to date view is that we should not drop the inline assembly |
|
Great job looking into this. I did not expect GCC to behave like this (really, removing register-register moves is a simple compiler routine). |
The generic fallback for
umul_ppmm/smul_ppmminsrc/longlong.hbuilds a 64x64 -> 128 product out of four half-word multiplications. Where the compiler has a 128-bit integer type, one widening multiply does the same job. Platforms that already have inline asm forumul_ppmmare untouched: the new block sits behind!defined(umul_ppmm), like the generic one it precedes.This is not a rare path, at least under gcc. Even with assembly enabled, gcc has inline asm for
umul_ppmmonly on amd64/i386 (longlong_asm_gcc.h:21) and arm/aarch64 (:247), so riscv64, ppc64, s390x, loongarch64 and mips64 always get the generic block; and./configure --disable-assemblyputs every architecture on it, x86-64 included. None of this applies to clang, which getslonglong_asm_clang.hunconditionally (src/longlong.h:47-48), outside theFLINT_WANT_ASSEMBLYgate.That last one makes the change easy to see without RISC-V hardware. On x86-64 with gcc, configure with
--disable-assemblyand preprocess a file that uses the macro, saysrc/ulong_extras/mulmod_preinv.c: before the patchumul_ppmmexpands to the four-multiplication version (grep -c __x0gives 8,__rx0), after it to a single__uint128_tproduct (__rx8,__x00). Both configurations build and passmake checkforulong_extras,nmod,nmod_mat,nmod_vec,fmpzandmpn_extras: 355 tests, same count either way.FLINT already uses the same widening multiply on the generic GNU path elsewhere:
n_mulhi(src/ulong_extras.h:47-48),ull_t(src/nmod.h:296-299) and_mul(src/crt_helpers.h:309-315). The guard here is copied fromcrt_helpers.h:309.For gcc this also completes the
umul_ppmmandsmul_ppmmitems ticked off in #1636, which until now only took effect for clang throughlonglong_asm_clang.h:194-195.Codegen
gcc 16,
-O2, counting the finalret:imulmul-march=rv64gcmulmul+mulhusmul_ppmmon riscv64 goes from 32 instructions and 4multomul+mulh. Bothmulhuandmulhbelong to the base M extension, so nothing beyond a stock rv64gc build is needed.Benchmark
SiFive U74 (VisionFive 2, rv64gc), gcc 16 cross build, governor pinned to
performanceat 1.5 GHz, master and patched runs interleaved, 4 runs each, spread <= 0.4%. Modulus a 60-bit prime:nmod_mat_mul, 200x200nmod_poly_gcd, length 800nmod_poly_mul, length 4000The gain is not uniform. It is large where
nmod_muldominates the inner loop and almost nothing for the polynomial routines, which spend their time elsewhere.Both libraries were built from 00e660a with
./configure --host=riscv64-linux-gnu --disable-shared --disable-pthread, with onlysrc/longlong.hdiffering between them.Division is deliberately left alone: the same treatment applied to
udiv_qrnndmakes gcc emit calls to__udivti3and__umodti3instead of the hardware divide, so it would be a pessimisation.Testing
Test suites cross-compiled for riscv64 and run natively on the U74 against the patched library:
test(which containst-umul_ppmmandt-smul_ppmm),nmod_mat,nmod,nmod_vec,ulong_extras,fmpz,mpn_extras. All exit 0, 371 PASS in total, no failures.Worth flagging: no CI job compiles the new branch. On every platform CI covers,
umul_ppmmis already defined before this point, by inline asm on x86-64/aarch64 with autotools, bylonglong_asm_clang.hfor clang, bylonglong_msc_*.hfor MSVC, while the 32-bit Alpine job has no__int128at all. The ways to exercise it are--disable-assemblyor an architecture without inline asm.One regression I found
On sparc64, gcc compiles the 128-bit product into a
call __multi3rather than inlining it (gcc 15.2 at-O2, also with-mcpu=niagara4). Every other target I checked inlines it: riscv64, ppc64, s390x, loongarch64, mips64el, aarch64. Since FLINT has no sparc CI and no sparc-specific code I left the guard as it is, but I am happy to add&& !defined(__sparc__)if you prefer.