当前位置:首页>>攻略文章>>正文
魔兽科普:模拟器组成原理(一)
2013-12-27 23:50:41 作者:fhsvengetta 来源: 浏览次数:0
摘要:我会把我所知道的重点尽可能地写出来。由于这个系统非常庞杂,不可能事无巨细全都写出来,但在你读懂这篇内容之后,加上少许编程功底,应该就能够达到自制DPS模拟器的水平。
导航:

 蒙特卡洛法重识SimC反向工程时间驱动和事件驱动表达式解析面向对象的模拟器设计机器不掷骰子|

机器不掷骰子
一次攻击,有可确定的c%的暴击率。那么同样条件下进行一次攻击,产生的结果可能有两种:c%的概率暴击,或者(1-c%)的概率不暴击。
同样,攻击是否未命中?是否被招架?躲闪?格挡?偏斜?
一次打出去的嗜血是否触发血涌?一次命中的肉搏是否触发杀戮机器?
你会说:我用rand()可以得到一个随机数,我用随机数落在哪个区间就可以确定攻击结果如何。
机器不掷骰子。你从未想象过计算机是如何得到随机数的。
这一部分将告诉你计算机生成随机数的原理,便于你理解模拟的底层工作机制。
如果你有兴趣编写自己的随机数生成器,在这一部分的后面,我们将以灰谷的ASA-128为例,浅谈一些高速随机数生成器常用的优化手法。

计算机获得随机数的方法,总体来说,由之前产生的随机数Rk-n、Rk-n+1、Rk-n+2、……Rk-2、Rk-1,通过某种杂化算法,推导得到新的随机数值Rk,并且表面上看起来尽可能与之前的序列无关。
说着很复杂,我们举个例子。
假如我们现在只需要得到1到7的随机数。
我们选用的杂化算法是“Rk=Rk-1+5”,如果结果超过7呢,就取一下余数。
比如第一个数是1,那么下一个数就是1+5=6;再下一个数就是6+5=11,11超过了7,所以取余得4。
那么由一个初值1可以得到序列1-6-4-2-7-5-3-……
由一个初值2可以得到序列2-7-5-3-1-……
别看这种算法简单,但是它确实有效,如果对其调节良好,它可以产生循环序列非常长的随机数,而且生成速度也比较快,足够提供给普通应用程序使用了。这种算法称为“线性同余法”,也是C库函数提供的rand()所采用的方法。上面例子中提供的初值(1、2)称为“种子值”,C库函数中使用srand()函数设置种子值。
种子值即初始状态。由于计算机系统是一个确定系统,由确定的初态执行确定的操作,一定会跳转到另一个确定的次态,所以得到的随机数序列只是看起来前后无关,其值其实是确定可求的,并非真正“随机”。
我们管这种随机数发生器(Random Number Generator,RNG)叫做伪随机数发生器(Pseudo RNG,PRNG)。你可以相信,在现有的计算机系统上,所有软件随机数发生器,一定都是伪随机数发生器。
在伪随机数发生器中,由相同的种子值,总是可以引出相同的随机数序列。

在SimC中,你可以通过关键字seed来设置随机数发生器的种子值。
Code (c):
seed=11223344

如果声明了deterministic_rng关键字,则SimC会使用固定的种子值31459。
Code (c):
deterministic_rng=1
#它等价于 seed=31459

如果你使用了多个线程,那么每个线程使用的种子值会递增1,以使它们获得不同的随机数序列。例如零号线程会使用31459,一号线程会使用31460,二号线程会使用31461……
固定了种子值,也就固定了RNG发生的随机数序列。如果包括“线程数”在内的各个模拟参数也都固定,你将得到固定的模拟结果。多次重复模拟,结果不会有一丝一毫的变化。
如果你不设置种子值,那么SimC会使用当前的Unix时间戳作为种子值。所以,你在不同时间进行的模拟,使用的种子值一定不一样,产生的随机数序列也将发生变化。最终现象就是,相同的条件,多次模拟,得到的模拟结果会上下浮动。
\

                 图:使用固定的种子值4262进行两次模拟,得到完全相同的结果144.1DPS。

现在工业和学术上的RNG标准,是采用单指令多数据流快速梅森旋转算法(SFMT),由广岛大学睦齐藤和松本诚所著。
SimC也不例外采用了SFMT。
[http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/SFMT/]
这种RNG产生的随机数质量高,速度快,基于SSE2指令集技术已经成熟,绝大多数现有PC平台都支持SSE2指令集。
随机数可以大量产生,而且迭代间算法相仿,所以非常适合采用单指令多数据流技术进行加速。

可能你会奇怪,老死程总是教导我们,不要轻易重写C库函数,因为它们都做了大量的优化,性能是最好的。
既然C库函数提供了rand(),那么我们为什么还要讨论随机数发生器?为什么还要有SFMT,而且SimC要采用第三方的RNG代替库函数?
那么很遗憾,编写库函数的人并非万能手。rand()在普通的应用中性能是非常出色的,例如你想在屏幕上随机画几条线做屏幕保护程序……
在高速运算中,rand()有三大不足:
精度低
rand()只能生成15比特的随机数,这连一个unsigned short类型都填充不满。要想使用rand()填满一个常用的数据类型,例如32比特的int或者尾码有52比特的double,你必须多次调用rand()将它们的结果拼接在一起。
这种拼接将招来极大的CPU开销,可以让生成速度放慢几倍甚至几十倍。
只能生成整数
rand()生成的是0~32767范围内的随机整数。在模拟中,我们往往都用0~1范围内的浮点数来表达概率。
由整数转型为0~1之间的浮点数的开销,比生成随机数本身的开销还要大。
速度慢
rand()并不会使用任何CPU扩展指令集进行加速。
生成精度低意味着我们需要多次调用它,你还需要蒙受额外的函数调用的开销。
调用库函数会中断编译器对你所写程序的优化。如果你把rand()插得代码里到处都是,你的程序几乎就得不到编译器的照顾了。
下面两张图是rand()和灰谷自己制作的随机数生成器ASA-128的生成速度对比。程序给出了Data Rate(数据填充速率),它们相差了几十倍。数据可能并不能非常准确地表达它们之间的性能差异,但足够直观。
\
\

                                                     图:C库函数rand与ASA-128生成速度对比。

SFMT的代码可以在上面给出的链接中找到。
这里我给出ASA-128的代码。要使用它,你应该遵从GNU GPLv3许可。
我简单对其中用到的优化手法进行说明,我们就结束RNG这一部分。
Code (c):
/**
    Aean"s SSE2-Accelerated 128-Bits Pseudo Random Number Generator.
                    Copyright (C) 2013 fhsvengetta
    License:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version. See <http://www.gnu.org/licenses/>.

    Notify:
        This program auto translate random bits into IEEE 754 double type.
    Each double-type random number will use 52-bit of data.
        A SSE2-compatible CPU is required at runtime.
*/

#include <emmintrin.h>
#include <malloc.h>

//=== PRNG Engine ===================================================
                //Force stack-alloc to enhance perf.
#define RNG_STACK_ALLOC
                //If you go on stack overflow, add /STACK:reserve option into link command line
                //, or undef this flag.

#ifndef RNG_STACK_ALLOC
#pragma message("RNG buffer is allocated at heap segment.")
#pragma message("To force stack-alloc for performance, define RNG_STACK_ALLOC in header then rebuild.")
#pragma message("RNG缓冲区被分配在堆上。要强制进行栈分配以提高性能,在头文件定义 RNG_STACK_ALLOC 然后重建。")
#endif

enum {RNG_BUFFER_SIZE = 4096};//Buffer size measured in __m128i (128bits). Must be divisible by 4.
class rng_t{
   private:
      __m128i           r0,r1,r2,r3;
#ifndef RNG_STACK_ALLOC
       unsigned int*   buffer_list;
#else
      unsigned int   buffer_list[RNG_BUFFER_SIZE*sizeof(__m128i)/sizeof(unsigned int)];
#endif
       size_t         buffer_i;
      void generate_buffer(void* bufptr);
   public:
      rng_t() : buffer_i(0){
#ifndef RNG_STACK_ALLOC
         buffer_list = ( unsigned int* )_aligned_malloc( RNG_BUFFER_SIZE * sizeof( __m128i ), 16 );
#endif
      };
#ifndef RNG_STACK_ALLOC
      ~rng_t(){
         _aligned_free(buffer_list);
      };
#endif
      double get(void);
      void set_seed(unsigned int seed);
};

//=== rng_generate_buffer ===========================================
void rng_t::generate_buffer( void* bufptr ) {
    __m128i* p     = ( __m128i* )bufptr;
    register __m128i m;
    register __m128i a0;
    register __m128i a1;
    register __m128i a2;
    register __m128i a3;
    register __m128i m0;
    register __m128i m1;
    register __m128i m2;
    register __m128i m3;
    size_t i = 0;

    //Iterate to fill buffer.
    for( ; i < RNG_BUFFER_SIZE; ) {
//--Loop0--
        //Mask Hybrid.
        m = _mm_set_epi16( 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200 );
        m0 = _mm_set_epi32( 0x1C001C00, 0x1C001C00, 0x1C001C00, 0x1C001C00 );
        m1 = _mm_set_epi32( 0x20002000, 0x20002000, 0x20002000, 0x20002000 );
        m2 = _mm_set_epi32( 0x40004000, 0x40004000, 0x40004000, 0x40004000 );
        m3 = _mm_set_epi32( 0x80008000, 0x80008000, 0x80008000, 0x80008000 );
        a0 = _mm_slli_epi16( r0, 10 );
        a1 = _mm_slli_epi16( r0, 9 );
        a2 = _mm_slli_epi16( r0, 8 );
        a3 = _mm_slli_epi16( r0, 3 );
        a0 = _mm_and_si128( a0, m0 );
        a1 = _mm_and_si128( a1, m1 );
        a2 = _mm_and_si128( a2, m2 );
        a3 = _mm_and_si128( a3, m3 );
        a0 = _mm_or_si128( a0, a1 );
        a2 = _mm_or_si128( a2, a3 );
        a0 = _mm_or_si128( a0, a2 );
        m = _mm_or_si128( m, a0 );
        //Random Number Masking.
        m0 = _mm_add_epi32( r0, r1 );
        m1 = _mm_add_epi32( r2, r3 );
        m0 = _mm_add_epi32( m0, m1 );
        a0 = _mm_srli_epi32( m0, 4 );
        m0 = _mm_xor_si128( m0, a0 );
        a1 = _mm_slli_epi16( m0, 2 );
        a1 = _mm_and_si128( a1, m );
        m0 = _mm_xor_si128( m0, a1 );
        a2 = _mm_slli_epi32( m0, 20 );
        a2 = _mm_and_si128( a2, m );
        m0 = _mm_xor_si128( m0, a2 );
        a3 = _mm_srli_epi32( m0, 18 );
        m0 = _mm_xor_si128( m0, a3 );
        //Store into register & memory.
        r0 = m0;
        _mm_stream_si128( &p[i++], m0 );
//--Loop1--
        //Mask Hybrid.
        m = _mm_set_epi16( 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200 );
        m0 = _mm_set_epi32( 0x1C001C00, 0x1C001C00, 0x1C001C00, 0x1C001C00 );
        m1 = _mm_set_epi32( 0x20002000, 0x20002000, 0x20002000, 0x20002000 );
        m2 = _mm_set_epi32( 0x40004000, 0x40004000, 0x40004000, 0x40004000 );
        m3 = _mm_set_epi32( 0x80008000, 0x80008000, 0x80008000, 0x80008000 );
        a0 = _mm_slli_epi16( r1, 10 );
        a1 = _mm_slli_epi16( r1, 9 );
        a2 = _mm_slli_epi16( r1, 8 );
        a3 = _mm_slli_epi16( r1, 3 );
        a0 = _mm_and_si128( a0, m0 );
        a1 = _mm_and_si128( a1, m1 );
        a2 = _mm_and_si128( a2, m2 );
        a3 = _mm_and_si128( a3, m3 );
        a0 = _mm_or_si128( a0, a1 );
        a2 = _mm_or_si128( a2, a3 );
        a0 = _mm_or_si128( a0, a2 );
        m = _mm_or_si128( m, a0 );
        //Random Number Masking.
        m0 = _mm_add_epi32( r0, r1 );
        m1 = _mm_add_epi32( r2, r3 );
        m0 = _mm_add_epi32( m0, m1 );
        a0 = _mm_srli_epi32( m0, 4 );
        m0 = _mm_xor_si128( m0, a0 );
        a1 = _mm_slli_epi16( m0, 2 );
        a1 = _mm_and_si128( a1, m );
        m0 = _mm_xor_si128( m0, a1 );
        a2 = _mm_slli_epi32( m0, 20 );
        a2 = _mm_and_si128( a2, m );
        m0 = _mm_xor_si128( m0, a2 );
        a3 = _mm_srli_epi32( m0, 18 );
        m0 = _mm_xor_si128( m0, a3 );
        //Store into register & memory.
        r1 = m0;
        _mm_stream_si128( &p[i++], m0 );
//--Loop2--
        //Mask Hybrid.
        m = _mm_set_epi16( 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200 );
        m0 = _mm_set_epi32( 0x1C001C00, 0x1C001C00, 0x1C001C00, 0x1C001C00 );
        m1 = _mm_set_epi32( 0x20002000, 0x20002000, 0x20002000, 0x20002000 );
        m2 = _mm_set_epi32( 0x40004000, 0x40004000, 0x40004000, 0x40004000 );
        m3 = _mm_set_epi32( 0x80008000, 0x80008000, 0x80008000, 0x80008000 );
        a0 = _mm_slli_epi16( r2, 10 );
        a1 = _mm_slli_epi16( r2, 9 );
        a2 = _mm_slli_epi16( r2, 8 );
        a3 = _mm_slli_epi16( r2, 3 );
        a0 = _mm_and_si128( a0, m0 );
        a1 = _mm_and_si128( a1, m1 );
        a2 = _mm_and_si128( a2, m2 );
        a3 = _mm_and_si128( a3, m3 );
        a0 = _mm_or_si128( a0, a1 );
        a2 = _mm_or_si128( a2, a3 );
        a0 = _mm_or_si128( a0, a2 );
        m = _mm_or_si128( m, a0 );
        //Random Number Masking.
        m0 = _mm_add_epi32( r0, r1 );
        m1 = _mm_add_epi32( r2, r3 );
        m0 = _mm_add_epi32( m0, m1 );
        a0 = _mm_srli_epi32( m0, 4 );
        m0 = _mm_xor_si128( m0, a0 );
        a1 = _mm_slli_epi16( m0, 2 );
        a1 = _mm_and_si128( a1, m );
        m0 = _mm_xor_si128( m0, a1 );
        a2 = _mm_slli_epi32( m0, 20 );
        a2 = _mm_and_si128( a2, m );
        m0 = _mm_xor_si128( m0, a2 );
        a3 = _mm_srli_epi32( m0, 18 );
        m0 = _mm_xor_si128( m0, a3 );
        //Store into register & memory.
        r2 = m0;
        _mm_stream_si128( &p[i++], m0 );
//--Loop3--
        //Mask Hybrid.
        m = _mm_set_epi16( 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200, 0x200 );
        m0 = _mm_set_epi32( 0x1C001C00, 0x1C001C00, 0x1C001C00, 0x1C001C00 );
        m1 = _mm_set_epi32( 0x20002000, 0x20002000, 0x20002000, 0x20002000 );
        m2 = _mm_set_epi32( 0x40004000, 0x40004000, 0x40004000, 0x40004000 );
        m3 = _mm_set_epi32( 0x80008000, 0x80008000, 0x80008000, 0x80008000 );
        a0 = _mm_slli_epi16( r3, 10 );
        a1 = _mm_slli_epi16( r3, 9 );
        a2 = _mm_slli_epi16( r3, 8 );
        a3 = _mm_slli_epi16( r3, 3 );
        a0 = _mm_and_si128( a0, m0 );
        a1 = _mm_and_si128( a1, m1 );
        a2 = _mm_and_si128( a2, m2 );
        a3 = _mm_and_si128( a3, m3 );
        a0 = _mm_or_si128( a0, a1 );
        a2 = _mm_or_si128( a2, a3 );
        a0 = _mm_or_si128( a0, a2 );
        m = _mm_or_si128( m, a0 );
        //Random Number Masking.
        m0 = _mm_add_epi32( r0, r1 );
        m1 = _mm_add_epi32( r2, r3 );
        m0 = _mm_add_epi32( m0, m1 );
        a0 = _mm_srli_epi32( m0, 4 );
        m0 = _mm_xor_si128( m0, a0 );
        a1 = _mm_slli_epi16( m0, 2 );
        a1 = _mm_and_si128( a1, m );
        m0 = _mm_xor_si128( m0, a1 );
        a2 = _mm_slli_epi32( m0, 20 );
        a2 = _mm_and_si128( a2, m );
        m0 = _mm_xor_si128( m0, a2 );
        a3 = _mm_srli_epi32( m0, 18 );
        m0 = _mm_xor_si128( m0, a3 );
        //Store into register & memory.
        r3 = m0;
        _mm_stream_si128( &p[i++], m0 );
    }
}

 

//=== rng_set_seed ==================================================
void rng_t::set_seed( unsigned int seed ) {
    short low = seed & 0xffff;
    short high = seed >> 16;
    r0 = _mm_set_epi16( high, high + 1, high, high + 2, high + 1, high, high + 2, high );
    r1 = _mm_set_epi16( low + 1, low, low + 2, low, low, low + 1, low, low + 2 );
    r2 = _mm_set_epi16( 5797, 4262, 8015, 3571, 12160, 6087, 6086, 7939 );
    r3 = _mm_set_epi16( 2306, 14613, 14614, 14615, 4849, 4850, 4851, 4852 );
}


//=== rng_get =======================================================
double rng_t::get( void ) {
    union                   {
        double as_double;
        unsigned int as_uint[2];
    } bit_to_double;
    if( buffer_i <= 1 ) {
        buffer_i = RNG_BUFFER_SIZE * sizeof( __m128i ) / sizeof( unsigned int ) - 1;
        generate_buffer( buffer_list );
    }
   /**
      All x86 architectures are little-endian.
      If you want to make this code running on big-endian processors,
      you should swap as_uint[1] / as_uint[0].
   */
    bit_to_double.as_uint[1] = ( buffer_list[buffer_i--] & 0x3FFFFFFF ) | 0x3FF00000;
    bit_to_double.as_uint[0] = buffer_list[buffer_i--];
    bit_to_double.as_double -= 1.0l;
    return bit_to_double.as_double;
}

其实代码并不长,SSE2代码的格式可能看起来比较难受,因为其实它已经属于一种内嵌汇编了。

ASA-128只输入序列中前四个随机数作为参数计算得到下一个。所以这里采用循环展开,在一次For循环中直接完成四组随机数,裁去了一个记录循环队列首尾位置的控制量。
关于循环展开,这是一种非常常见的性能优化手法,如果你不明白是什么意思,可以搜索一下相关资料,这里就不再浪费篇幅去讲。

针对CPU的流水线和乱序执行特性,指令在排序时需要尽可能降低上下文相依性(即下一条指令尽量不要用到上一条指令的结果)。
例如要计算s=a+b+c+d+e+f+g+h,正常的算法是:
s=a+b
s=s+c
s=s+d
s=s+e
s=s+f
s=s+g
s=s+h
但是这全部七条指令都前后相依。例如第一条指令中红色的s的值不计算完毕,就无法计算下一条指令的s,因为下一条指令需要红色的s作为一个操作数。
如果改为:
s=a+b
c=c+d
e=e+f
g=g+h
s=s+c
e=e+g
s=s+e
这时前四条指令都完全互相无关,直到第五条指令才用到第一条指令的结果,指令上下文相依性就变得很小,CPU流水线的效率才能得以最大程度地发挥。

由于生成的是随机比特,也就是整数。整数到浮点数的转换是一大开销热点。
普通的转换方法,是通过整数到浮点数的转型,和一次浮点除法完成的。例如:
Code (c):
int r_int = rand(); //r_int是[0, 32768)区间内的整数随机数。
double r_double = static_cast<double>(r_int); //由整数到浮点数的强制转型,现在r_double是[0.0, 32767.0]区间内的浮点随机数。
r_double = r_double / 32768.0l; //通过一次浮动除法,现在r_double是[0.0, 1.0)区间内的浮点随机数。
return r_double

整数到浮点数的转型开销大约在4~16个时钟周期,浮点除法需要20~45个时钟周期。
如果能够避免它们,转换速度将会呈几十倍的速度提升。

按照IEEE 754的双精度浮点数标准,浮点数1.0的二进制表示为:
0011 1111 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
而小于2.0的、双精度所能表示的最大浮点数1.9999999999999997779553950749686919152736663818359375,它的二进制表示为:
0011 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
我们注意到,[1.0, 2.0)区间内的双精度浮点数,固定由“0011 1111 1111”组成高12位;低52位从全0一直变化到全1。
所以如果我们刻意制造一块内存,在其高12位填充“0011 1111 1111”,然后在其低52位填充随机比特,就可以得到[1.0, 2.0)区间的浮点随机数了。
要得到[0.0, 1.0)区间的浮点随机数,只需做一次浮点减法,让其值自减1.0。浮点减法的开销是非常小的。
Code (c):
union                  {
    double as_double;
    __int64 as_int;
}r; //union                  联合体,同一块内存空间r,既可以解释为64位整数,也可以解释为双精度浮点数。
//这样我们就能对浮点数进行按位逻辑运算,直接操作其中的比特。正常情况下,只有整数才接受按位逻辑运算。
r.as_int = rand(); //假设rand()提供了64比特的随机数据,按照整数存入r中。
//我们现在把它按照IEEE 754的双精度浮点数标准进行格式化。
r.as_int = ( r.as_int & 0x3FFFFFFFFFFFFFFF ) | 0x3FF0000000000000; //通过两次位逻辑运算,将r的高12位(1位符号位,11位阶码)
                                                             //设置为“0011 1111 1111”。后52位尾码维持不变。
                                                             //现在r就是[1.0, 2.0)区间内的浮点随机数了。
r.as_double -= 1.0l; //做一次浮点减法,就得到了[0.0, 1.0)区间内的浮点随机数。
return r.as_double;

这种转换的开销只有一次位与运算,一次位或运算,和一次浮点减法。它们都是开销最小的计算,每条指令都可在1个时钟周期内完成。

小结
这个系统太大了,如果要全部写下来,恐怕要成为一本教科书。
内容庞杂,思路不是很清晰,这次写出来的东西像是一块一块的东西拼凑起来的。篇幅看起来很长,其实不到五万字。
可能遗漏了很多应该提及的内容,但我现在已经不知道再讲些什么好了。讲SimC属性权值是如何评的?拜托这你自己应该弄明白的。
所以先只来这么多。如果有必要,我再看情况往这里添加内容,或者干脆再来一个模拟器组成原理第二篇。

对于有心了解模拟器技术的人,这可能足够他们入门,至少是了解一点模拟器的思想。
更加展开的内容,你可以试试点击主楼的参考文献,它们指向四面八方,包含各式各样的资料。
有些只是引用了一句话;有些则是本应出现在文中却因篇幅问题被挤在外面的科学论文。




相关报道:

[关闭] [返回顶部]


  返回首页 | 最新资讯 | 资源下载 | 魔兽图片 | 单机文档 | 技术攻略 | 玩家视频
备案号:蜀ICP备2024062380号-1
免责声明:本网站为热爱怀旧WOW的玩家们建立的魔兽世界资料网站,仅供交流和学习使用,非盈利和商用.如有侵权之处,请联系我们,我们会在24小时内确认删除侵权内容,谢谢合作。
Copyright © 2024 - 2024 WOWAII.COM Corporation, All Rights Reserved

机器人国度