| [Назад] [Далее] | ![]() |
Самый часто применяемый тип алгоритмов генерации псевдослучайных последовательностей — линейные конгруэнтные генераторы, описываемые общим рекуррентным соотношением:
Ij+1 = (aIj + с) MOD m
При правильно выбранных числах а и с эта последовательность возвращает все числа от нуля до m–1 псевдослучайным образом и ее периодичность сказывается только на последовательностях порядка m. Такие генераторы очень легко реализуются и работают быстро, но им присущи и некоторые недостатки: самый младший бит намного менее случаен, чем, например, самый старший, а также если попытаться использовать результаты работы этого генератора для заполнения k-мерного пространства, начиная с некоторого k, точки будут лежать на параллельных плоскостях. Оба эти недостатка можно устранить, используя так называемое перемешивание данных: числа, получаемые при работе последовательности, не выводятся сразу, а помещаются в случайно выбранную ячейку небольшой таблицы (8 – 16 чисел), а число, находившееся в этой ячейке раньше, возвращается как результат работы функции.
Если число а подобрано очень тщательно, может оказаться, что число с равно нулю. Так, классический стандартный генератор Льюиса, Гудмана и Миллера использует а = 16 807 (75) при m = 231–1, а генераторы Парка и Миллера используют а = 48 271 и а = 69 621 (при том же m). Любой из этих генераторов можно легко использовать в ассемблере для получения случайного 32-битного числа, достаточно всего двух команд — MUL и DIV.
; Процедура rand
; возвращает в ЕАХ случайное положительное 32-битное число
; (от 0 до 231-2)
;
rand proc near
push edx
mov eax,dword ptr seed ; считать последнее
; случайное число
test eax,eax ; проверить его, если это -1,
js fetch_seed ; функция еще ни разу не
; вызывалась и надо создать
; начальное значение
randomize:
mul dword ptr rand_a ; умножить на число а,
div dword ptr rand_m ; взять остаток от
; деления на 231-1
mov eax,edx
mov dword ptr seed,eax ; сохранить для
; следующих вызовов
pop edx
ret
fetch_seed:
push ds
push 0040h
pop ds
mov eax,dword ptr ds:006Ch ; считать
; двойное слово из области
pop ds ; данных BIOS по адресу
; 0040:0060 - текущее число
jmp short randomize ; тактов таймера
rand_a dd 69621
rand_m dd 7FFFFFFFh
seed dd -1
rand endp
Если период этого генератора (порядка 109) окажется слишком мал, можно скомбинировать два генератора с разными а и m, не имеющими общих делителей, например: a1 = 400 014 с m1 = 2 147 483 563 и а2 = 40 692 с m2 = 2 147 483 399. Генератор, работающий по уравнению
Ij+1 = (a1Ij + a2Ij) MOD m,
где m — любой из m1 и m2, имеет период 2,3 * 1018.
Очевидный недостаток такого генератора — команды MUL и DIV относятся к самым медленным. От DIV можно избавиться, используя один из генераторов с ненулевым числом с и с m, равным степени двойки (тогда DIV m заменяется на AND m–1), например: а = 25 173, с = 13 849, m = 216 или a = 1 664 525, с = 1 013 904 223, m = 232, но проще перейти к методам, основанным на сдвигах или вычитаниях.
Алгоритмы, основанные на вычитаниях, не так подробно изучены, как конгруэнтные, но они достаточно широко используются из-за своей скорости и, по-видимому, не имеют заметных недостатков. Подробное объяснение алгоритма этого генератора (а также алгоритмов многих других генераторов случайных чисел) приведено в книге Кнута Д.Е. «Искусство программирования» (т. 2).
; Процедура srand_init
; инициализирует кольцевой буфер для генератора, использующего вычитания
; ввод: ЕАХ - начальное значение, например из области
; данных BIOS, как в предыдущем примере
srand_init proc near
push bx
push si
push edx
mov edx,1
; засеять кольцевой буфер
mov bx,216
do_0: mov word ptr ablex[bx],dx
sub eax,edx
xchg eax,edx
sub bx,4
jge do_0
; разогреть генератор
mov bx,216
do_1: push bx
do_2: mov si,bx
add si,120
cmp si,216
jbe skip
sub si,216
skip: mov eax,dword ptr tablex[bx]
sub eax,dword ptr tablex[si]
mov dword ptr tablex[bx},eax
sub bx,4
jge do_2
pop bx
sub bx,4
jge do_1
; инициализировать индексы
sub ax,ax
mov word ptr index0,ax
mov ax,124
mov index1,ax
pop edx
pop si
pop bx
ret
srand_init endp
; процедура srand
; возвращает случайное 32-битное число в ЕАХ (от 0 до 232-1)
; перед первым вызовом этой процедуры должна быть один раз вызвана
; процедура srand_init
srand proc near
push bx
push si
mov bx,word ptr index0
mov si,word ptr index1 ; считать индексы
mov eax,dword ptr tablex[bx]
sub eax,dword ptr tablex[si] ; создать новое
; случайное число
mov dword ptr tablex[si],eax ; сохранить его
; в кольцевом буфере
sub si,4 ; уменьшить индексы,
jl fix_si ; перенося их на конец буфера,
fixed_si:
mov word ptr index1,si ; если они выходят
; за начало
sub bx,4
jl fix_bx
fixed_bx:
mov index0,bx
pop si
pop bx
ret
fix_SI: mov si,216
jmp short fixed_SI
fix_BX: mov bx,216
jmp short fixed_BX
srand endp
tablex dd 55 dup (?) ; кольцевой буфер случайных чисел
index0 dw ? ; индексы для кольцевого буфера
index1 dw ?
Часто бывает, что требуется получить всего один или несколько случайных бит, и генераторы, работающие с 32-битными числами, оказываются неэффективными. В этом случае удобно применять алгоритмы, основанные на сдвигах:
; rand8
; возвращает случайное 8-битное число в AL,
; переменная seed должна быть инициализирована заранее,
; например из области данных BIOS, как в примере
; для конгруэнтного генератора
rand8 proc near
mov ax, word ptr seed
mov cx,8
newbit: mov bx,ax
and bx,002Dh
xor bh,bl
clc
jpe shift
stc
shift: rcr ax,1
loop newbit
mov word ptr seed,ax
mov ah,0
ret
rand8 endp