The C memory aliasing model allows certain kinds of optimizations.
For example, consider the following code that's generated at the end the
main loop of the FFT in _num.scm (after pulling it out of ##bignum.*):
___F64VECTORSET(___STK(-5),___R3,___F64V53)
___F64VECTORSET(___STK(-5),___R4,___F64V52)
___F64VECTORSET(___STK(-5),___STK(1),___F64V51)
___F64VECTORSET(___STK(-5),___STK(2),___F64V50)
___F64VECTORSET(___STK(-5),___STK(3),___F64V49)
___F64VECTORSET(___STK(-5),___STK(4),___F64V48)
___F64VECTORSET(___STK(-5),___STK(5),___F64V47)
___F64VECTORSET(___STK(-5),___STK(6),___F64V46)
___SET_R3(___FIXADD(___R3,___FIX(2L)))
___POLL(9)
___DEF_SLBL(9,___L9_direct_2d_fft_2d_recursive_2d_4)
___GOTO(___L22_direct_2d_fft_2d_recursive_2d_4)
Here we're alternating loading elements off the stack and storing
floating-point values in a vector.
Under C's memory aliasing model (i.e., with -fstrict-aliasing, the gcc
default), STK(-5) can be loaded into a register and reused after each
floating-point store, because gcc can assume that the floating-point
store does not change the value of STK(-5), since WORDs and doubles are
different types.
When we call gcc with -fno-strict-aliasing, then the compiler has to
assume that the floating-point store may affect the value of STK(-5), so
it has to reload STK(-5) before each floating-point store.
This has the following effect on the time to compute 10,000 forward and
10,000 backward complex FFTs of 1,024 elements.
With -fno-strict-aliasing (used now by Gambit for reasons I'll explain
below):
(time (do ((i 0 (fx+ i 1))) ((fx= i 10000)) (direct-fft-recursive-4 a
table) (inverse-fft-recursive-4 a table) (do ((i 0 (fx+ i 1))) ((fx= i
(fx* two^n 2))) (f64vector-set! a i (fl/ (f64vector-ref a i)
inexact-two^n)))))
705 ms real time
704 ms cpu time (704 user, 0 system)
6 collections accounting for 2 ms real time (0 user, 0 system)
1600000 bytes allocated
22 minor faults
no major faults
With -fstrict-aliasing:
(time (do ((i 0 (fx+ i 1))) ((fx= i 10000)) (direct-fft-recursive-4 a
table) (inverse-fft-recursive-4 a table) (do ((i 0 (fx+ i 1))) ((fx= i
(fx* two^n 2))) (f64vector-set! a i (fl/ (f64vector-ref a i)
inexact-two^n)))))
590 ms real time
592 ms cpu time (588 user, 4 system)
6 collections accounting for 2 ms real time (4 user, 0 system)
1600000 bytes allocated
22 minor faults
no major faults
That's a 19% performance improvement. (And the performance isn't bad,
1.74 GFLOPS on my 2.33GHz Core 2 quad 6600. Later I'll try to get gcc
to vectorize the main loop.)
As I understand it, the C memory aliasing model is that once you store
something of a certain type into a memory location, then you agree that
everything you read from that memory location and everything else you
write to that memory location will be of the same type.
So why do we need -fno-strict-aliasing? I know of two reasons: (1) in
gambit.h we read and write bignums as adigits, mdigits, or fdigits, and
these are chunks of the bignums of different sizes (generally 64/32/8
bits on 64-bit machines, and 64/16/8 bits on 32-bit machines) and (2) I
believe we sometimes access double-precision floating-point numbers as
integers. Marc, maybe you know of other instances in the code.
Both of these things can be fixed by accessing the internals of a bignum
through a pointer type to a union of vectors of integers of the
appropriate bit sizes, and doing the same for double-precision floats.
(I had a much more difficult plan in mind before, of coming up with a
union type of all possible Scheme objects, but that isn't necessary, I
see now.)
I've been trying to figure out how to make the changes to gambit.h to do
this in a clean way but without luck. Marc, you're the über programmer,
do you have time to look at this?
Brad