Here's a bit of code I just wrote. It's a random-number generator, simple but a pretty good one. (I borrowed it from
docs.microsoft.com/en-us/archive/msdn-magazine/2016/august/test-run-lightweight-random-number-generation on MSDN; it's the Lehmer algorithm from that article.)
;============================================
; LehmerRand - Random Number Generator
;
; Contains following routines:
;
; LehmerRandInit()
; Initializes RNG. Not necessary to call it, as it's
; automagically called the first time you call LehmerRand().
;
; LehmerRand (range)
; Returns a random # in the range [0 ... range-1].
;============================================
include \masm32\include\masm32rt.inc
;============================================
; Defines, macros, prototypes, etc.
;============================================
;===== RNG values: =====
$a EQU 16807
$m EQU 2147483647
$q EQU 127773
$r EQU 2836
;============================================
; HERE BE DATA
;============================================
.data
LehmerInitFlag DB FALSE
;============================================
; UNINITIALIZED DATA
;============================================
.data?
QPCvalue LABEL DWORD
RandomSeed LABEL DWORD ;Same as low part of counter.
QPClow DD ?
QPChigh DD ?
X87CW DW ?
OldX87CW DW ?
;============================================
; CODE LIVES HERE
;============================================
.code
;====================================================================
; LehmerRandInit()
;
; Sets up RNG for use by getting initial seed from hi-res timer.
;====================================================================
LehmerRandInit PROC
; Seed the RNG from a time value:
INVOKE QueryPerformanceCounter, OFFSET QPCvalue
AND QPClow, 7FFFFFFFH ;Make sure seed doesn't overflow signed INT.
MOV LehmerInitFlag, TRUE
RET
LehmerRandInit ENDP
;====================================================================
; LehmerRand (range)
;
; Delivers a random number, based on the "seed".
; Automatically initializes the seed the first time it's called
; (user can also re-initialize by calling LehmerRandInit(), above)
;
; This code is derived from the MSDN article "Lightweight Random
; Number Generation"
; (https://docs.microsoft.com/en-us/archive/msdn-magazine/2016/august/test-run-lightweight-random-number-generation)
; This is the Lehmer algorithm flavor.
;
; range:
; Upper limit of random #s desired (#s will be between 0 and [range-1]).
;
; Returns:
; EAX = random number
;====================================================================
LehmerRand PROC range:DWORD
LOCAL hi:DWORD, lo:DWORD, temp:DWORD
; Automagically initialize ourselves the 1st time through:
CMP LehmerInitFlag, TRUE
JE @F
CALL LehmerRandInit
@@: MOV EAX, RandomSeed
XOR EDX, EDX
MOV ECX, $q
DIV ECX
MOV hi, EAX ;hi = seed / q.
MOV lo, EDX ;lo = seed % q.
; seed = a*lo - r*hi:
MOV EAX, $a
MUL lo
MOV ECX, EAX ;Stash temporarily.
MOV EAX, $r
MUL hi
SUB ECX, EAX
JNC @F ;Is < 0?
ADD ECX, $m ; Yep, so correct.
@@: MOV RandomSeed, ECX ;New seed for next time.
FINIT ;Reset FPU (to clear stack regs.)
; Set rounding control field to "round down":
FSTCW X87CW
MOV AX, X87CW
MOV OldX87CW, AX ;Save current CW.
AND X87CW, 0F3FFH ;Clear RC field.
OR X87CW, 400H ;Set RC field = 01.
FLDCW X87CW
; Rand = seed / m:
FILD RandomSeed
MOV temp, $m
FILD temp
FDIV
; Scale random # to range:
FILD range
FMUL
FISTP temp
MOV EAX, temp
FLDCW OldX87CW ;Restore original CW.
RET
LehmerRand ENDP
END
It makes some use of the FPU. My questions:
1. When I first tested this it would run fine 7 times but would fail on the 8th time, with an error from the FPU. Turned out I was using up all of the "stack" registers, which is why there's a FINIT in there now, to clear them. Is this the best way to do that? the only way? how else can you clear FPU registers? just store something (FSTP/FISTP) into a bit bucket? I thought I saw some instructions,
xxxPP that did a double pop; could one use these? And is FINIT an expensive instruction, time-wise? Seems kind of drastic to do a total reset on the poor FPU every time through this routine.
2. Regarding FPU "etiquette": I needed to change the rounding mode here to "round down" so that the result would always be less than the "range" variable. That works fine. I took the trouble to save the existing code word and restore it afterwards. Is this necessary? It seems like the FPU is a resource available to any process that wants to use it, so I get the feeling that it's not that important to carefully save its complete state and then restore it when you're done. I mean, it's not like having to save and restore those "sacred" X86 registers (EBX, ESI, EDI, EBP), is it? Isn't each process responsible for setting up the FPU to its liking, with no assumptions about what state it's in?
I'm hoping Ray Filiatreault might drop by here, seeing as he's the resident FPU guru here ...
(BTW, regarding the Lehmer RNG, it's pretty good, but as the author points out it's not statistically rigorous and isn't suitable for applications like cryptographic security.)