实验比较各离散采样算法

回应CSDN肖舸《做程序,要“专注”和“客观”》,实验比较各离散采样算法

2010-05-27 23:57 by Milo Yip, 17412 阅读, 69 评论, 收藏, 编辑

自从肖舸在其CSDN博客上说“拒绝回答博客园等网站网友的问题”,实质上不单是拒绝回答,而且还删去包括一些网友及本人对于纯粹技术探讨的评论。当然每位博主都有自由这么做,但个人认为这对于社区的交流发展有负面影响。为了探讨这个技术问题,本人唯有把回应发表于博客园内。本文会阐述之前的论点,评论肖舸的实现,并进行了兩个实验比较不同算法、实现的优劣之处。

之前的“交流”

约一个月前,肖舸于《实际中常用的一个随机数产生器(分类别概率随机)》(下简称《实》)一文中,介绍了一个宣称“0bug”的实现。而实质上这实现至少有两个bug,其中一个已被网友发现,而肖舸也随后更新,不过没有公开向该网友致谢,连网友的id或名字也没写。本人还发现另一个会做成崩溃bug,稍后再说。

因为有感《实》欠缺背后理论,而且该实现的需求不清楚、代码难读,所以本人于5日后撰写博文《用JavaScript玩转游戏编程(一)掉宝类型概率》(下简称《用》 ),从统计学解释这问题,并以JavaScript实作互动的示范程序。文中提及:

这里用了线性搜寻(linear search),……另外,也可以用二分搜寻(binary search),那么复杂度会降低为O(lg N),……那么,还有没有更快的方法呢?答案是肯定的,例如别名方法(alias method)、近似方法等,有兴趣的读者可参考……当然,在N很小的情况下,线性搜寻和二分搜寻也足够。 
笔者撰写本文,灵感来自这篇博文(指《实》)。其算法实际上是储存CDF的逆函数采样,利用空间和有限的CDF精确度,换取O(1)的时间复杂度。衡量N的大小、精确度、空间需求、缓存延迟后,或许该方法也能适合某些个别需求。但对于该文作者说N最大为100,二分搜寻只需最多7次迭代,因缓存问题可能二分搜寻更快。

之后,肖舸在另一篇博文《做程序,要“专注”和“客观”》中,当中有一段似乎是回应上文:

就好比我前面写的一篇博文《实》,我在文中的代码里,明明实现了O(1)的复杂度,但是就有人,为了攻击我本人和我的书《……》,专门撰文,说用其他办法,O(7)的复杂度也可以实现,我这个办法不值得提倡。
我晕,我们做算法优化,有时候,O(n)这个值,能减少1都是巨大的成功,因为程序是有循环的,循环次数是被乘数哦,这是乘法关系,这个核心算法复杂度减少1,放出去就是几千万甚至几亿的时钟开销,效率提升就是巨大的。很多时候,我做优化,都在为了减少这个1在努力。 
不过,这毕竟是少数人,准确的讲,说这话的人不能算技术人员,因为针对到科学的,算法的,优化的问题上,一是一,二是二,不能带着个人感情讨论。这是技术人员,特别是程序员基本的职业道德。 
这里,我希望广大程序员朋友一定要养成一个习惯,“客观”和“严谨”是程序员的基本职业修养,也是我们能在这个行业里面立足的根本,千万不要丢掉了。


因为文中说“O(7)”,我想应该是回应我写的7次迭代,如果不是, 就当本人对号入座吧。对于算法是否属于“科学”,大家可以想想。我自问只谈到技术上的意见,不知道为什么会说到“个人感情”,甚至推理至是否一个技术人员、有没有职业道德问题了。
也许是本人写得不够清楚,也许是读者有误解,无论如何,本人也在此解释一下。

复杂度与性能

首先,肖舸说“O(n)这个值”、“ 核心算法复杂度减少1” 、“O(7)”,都是不正确的说法。

研究算法的运行时间,最常见是采用Big-O表示法,例如O(1)、O(n)等。这表示法是指,算法在n接近无限大的时候,其运行时间的渐近复杂度(asymptotic complexity)的上界(upper bound)。举个例子,例如解决同一问题的两个算法﹐,它们的运行时间(例如单位是秒)为:

用Big-O表示法,A算法是线性速度O(n),B算法是常数速度O(1)。也就是说n大到某个程度,算法A比算法B慢。那么n要有多大?只要联合两个函数,即可求出,当n > 4时,算法A比B慢。相反,当n<4时,算法A会比算法B快。如果有另一组算法,其方程求出来的n是少于或等于0,说明当中的其中一个算法永远比另一个优。

使用Big-O表示法的目的,是方便比较不同的算法。 Big-O会隐藏的算法实现时的系数(例如上面例子中的2、1、9)。而在实际应用中,设计或实现算法,还要考虑多个因素,例如

  • 算法的应用场合: 算法所需的精确度、时间要求(通常二者须此消彼长)。如算法分为几个步骤(如本问题可分割为初始化和采样),每个步骤的实际使用次数。另外亦要考虑输入数据的分布。
  • 内存的使用: 同时间内所需的内存最大值(可用Big-O表示法)、内存存取模式(access pattern)。
  • 算法实现的难度: 除了开发及维护时间,还影响程序是否健壮(robust),例如复杂的浮点运算会做成误差。
  • 硬件: 例如主频、核心数量、内存速度、特殊指令(如SIMD),或者不同架构的可编程系统(如GPU)、设备等
  • 软件: 会执行于什么操作系统、编译器、库等等,这些软件对算法是否有影响。

很多时候,解决同一问题有许多算法,并没有绝对的好坏之分(当然有时候也有些能完全取胜,有些完全无用),每个算法都其优点缺点。程序员须要分析实际应用,去选择及实现算法。理想地,更可以实验多个算法,用实际数据去作出比较。

评论肖舸的实现

这个是肖舸在《实》公布的实现代码(本人加入了#ifdef MILO_HOTFIX及GetMemory()):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#define CTonyRandomArea_TOKEN_MAX 100           //最大类型数  
#define CTonyRandomArea_TOKEN_AREA_MAX 10000    //类型数组单元数,精确到小数点后两位  
//输入最大100个元素的数组,每个数组表示每类占有的百分比,内部自带百分比调整。  
//即如果外部输入的数字之和不是整数100,内部会根据百分比,自动调整其比例,使总和=100.0  
//然后内部建立10000个单元的类型数组,根据传入的每种类型的比例,在类型数组中批量填充对应的类型值  
//总之,类型数组中每种类型的数量,占据的比例正好是输入的百分比  
//最后,在0~10000中取随机,然后在对应的类型数组单元中取类型值,即为返回的类型  
class  CTonyRandomArea  
{  
public :  
     CTonyRandomArea( double * pTokenPercentArray, char  cTokenCount)  
     {  
         m_nTokenCount=cTokenCount;  
         if (CTonyRandomArea_TOKEN_MAX<m_nTokenCount)  
             m_nTokenCount=CTonyRandomArea_TOKEN_MAX;  
         int  i=0;  
         for (i=0;i<m_nTokenCount;i++)  
         {  
             m_dTokenPercentArray[i]=*(pTokenPercentArray+i);  
         }  
         //动态调整内部的值  
         //有时候试验人员,测得几个状态出现的数字,可能懒得再计算成百分比  
         //程序帮忙自动计算  
         double  dNumberCount=0;  
         for (i=0;i<m_nTokenCount;i++)  
         {  
             dNumberCount+=m_dTokenPercentArray[i];  
         }  
         if (100.0!=dNumberCount)  
         {  
             for (i=0;i<m_nTokenCount;i++)  
             {  
                 m_dTokenPercentArray[i]/=dNumberCount;
                 m_dTokenPercentArray[i]*=100;
             }  
         }  
         //以小数点后两位精度,开始计算在10000个总单元中,每种类型对应的数量。  
         for (i=0;i<m_nTokenCount;i++)  
         {  
             m_sTokenPercentArray[i]=( short )(m_dTokenPercentArray[i]*100);  
         }  
   
         //按比例填充类型数组  
         int  j=0;  
         int  nFillMin=0;  
         int  nFillMax=0;  
 
#ifdef MILO_HOTFIX
         for (i=0;i<CTonyRandomArea_TOKEN_AREA_MAX;i++)
         {  
             m_cTokenPercentArrayAreaUp[i]=0;
         }
#else
         for (i=0;i<m_nTokenCount;i++)
         {
             m_cTokenPercentArrayAreaUp[i]=-1;  
         }
#endif
   
         for (i=0;i<m_nTokenCount;i++)  
         {  
             nFillMax=nFillMin+m_sTokenPercentArray[i];  
             for (j=nFillMin;j<nFillMax;j++)  
             {  
                 m_cTokenPercentArrayAreaUp[j]=i;  
             }  
             nFillMin=nFillMax;  
         }  
//      m_cTokenPercentArrayAreaUp[CTonyRandomArea_TOKEN_AREA_MAX-1]=i-1;  
     }  
     ~CTonyRandomArea(){}  
     void  PrintfInfo( void )  
     {  
         int  i=0;  
         double  dNumberCount=0;  
         for (i=0;i<m_nTokenCount;i++)  
         {  
             dNumberCount+=m_dTokenPercentArray[i];  
             printf ( "%d = %f\n" ,i,m_dTokenPercentArray[i]);  
         }  
         printf ( "All = %f\n" ,dNumberCount);  
   
         //打印10000个单元的类型分布,看看排得对不对  
         //这段打印起来太长,一般隐掉,需要了再打印  
//      int nOutMax=400;  
//      int nInMax=25;      //二者相乘为10000  
//      int j=0;  
//      for(i=0;i<nOutMax;i++)  
//      {  
//          printf("%05d - ",i*nInMax);  
//          for(j=0;j<nInMax;j++)  
//          {  
//              printf("%d ",m_cTokenPercentArrayAreaUp[i*nInMax+j]);  
//          }  
//          printf("\n");  
//      }  
     }  
   
     //取类型数组对应单元的值  
     char  GetType( int  nTypeIndex)    //输入参数0~10000  
     {  
         if (10000<=nTypeIndex) nTypeIndex=9999;  
         if (0>nTypeIndex) nTypeIndex=0;  
         return  m_cTokenPercentArrayAreaUp[nTypeIndex];  
     }  
     //真实的工作函数,利用输入的概率来产生随机type  
     char  GetRandomType( void )  
     {  
         return  GetType(GetRandomBetween(0,10000));  
     }  
 
     // MILO ADD {
     size_t  GetMemory() const  {
         return  sizeof (* this );
     }
     // } MILO
 
private :  
     double  m_dTokenPercentArray[CTonyRandomArea_TOKEN_MAX];             //比例数组  
     short  m_sTokenPercentArray[CTonyRandomArea_TOKEN_MAX];              //比例数组,短整型,1~10000,相当于精确到小数点后两位  
     char  m_nTokenCount;  
     char  m_cTokenPercentArrayAreaUp[CTonyRandomArea_TOKEN_AREA_MAX];    //类型数组  
};
////这是测试代码  
//void TestCTonyRandomArea(void)  
//{  
//    double dTokenPercentArray[100];  
// 
//    dTokenPercentArray[0]=12.0;  
//    dTokenPercentArray[1]=40.0;  
//    dTokenPercentArray[2]=40.0;  
//    dTokenPercentArray[3]=7.0;  
//    dTokenPercentArray[4]=1.0;  
// 
//    CTonyRandomArea Area1(dTokenPercentArray,5);  
//    Area1.PrintfInfo();  
// 
//    int i=0;  
//    for(i=0;i<20;i++)  
//    {  
//        printf("RandType = %d\n",Area1.GetRandomType());  
//    }  
//}

崩溃Bug

在测试这个实现时,发现GetRandomType()返回的值会>=N,而N为概率数组的大小。这样有机会做成程序崩溃。例如在代码中,把返回值作为索引,填进数组就会写到不合法的地址。

调试之后,发现程序的bug位于三个地方。第一,21-36行检查概率总和是否等于100个百分点,否则会自动进行正规化(normalization)。正规化后,浮点小数只能表示近似值。第二,在37-41行会把浮点小数乘上10000,再转做整数。因为这里是下取整,计算出来的整数总和有机会少于10000。于是m_cTokenPercentArrayAreaUp数组最后的几个元素未被填入。第三,第54-57行对m_cTokenPercentArrayAreaUp数组进行初始化,但用错循环次数,应为CTonyRandomArea_TOKEN_AREA_MAX而不是m_nTokenCount。

例如使用{ 3, 5, 7, 11, 13 } 作为输入,CTonyRandomArea_TOKEN_AREA_MAX数组就会填入前9998个元素,最后两个未被初始化。

这三个错处要完全更正会改动太多,不如重写。所以本人只是作最少改动,原整地初始化CTonyRandomArea_TOKEN_AREA_MAX数组为0。

用肉眼检验正确性?

测试函数TestCTonyRandomArea()的作用就是列印20个采样。该程序员或测试人员是用肉眼观测那20个采样,看看是否大概和输入的概率分布接近么?或是记下每个返回值的频率,笔算其频率除以20后的百分比?

这个问题本质上就是能编写测试代码去自动测试的。以下的实验就会测试各个实现的特性,包括其误差。

浪费内存

m_dTokenPercentArray和m_sTokenPercentArray都没必要作为成员变量。前者在PrintfInfo()函数还有一点用途,后者只在构造函数使用。

const-ness

构造函数的指针没加const。使用者用的时候要const_cast。 C语言的函数都要适当地使用const指针(可参考strlen原型),何况是C++呢。

乱数产生器不均分布

以下是其的乱数产生器代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
inline  int  _GetNot0( void )  
{  
     int  nRet= rand ();  
     if (!nRet) nRet++;  
     return  nRet;  
}  
inline  int  GetRandomBetween( int  nBegin, int  nEnd)  
{  
     int  n=_GetNot0();  
     int  nBetween=0;  
     if (0>nBegin) nBegin=-nBegin;  
     if (0>nEnd) nEnd=-nEnd;  
     if (nBegin>nEnd)  
     {  
         nBetween=nEnd;  
         nEnd=nBegin;  
         nBegin=nBetween;  
     }  
     else  if (nBegin==nEnd)  
         nEnd=nBegin+10;  
     nBetween=nEnd-nBegin;  
     n=n%nBetween;  
     n+=nBegin;  
     return  n;  
}

这个在书评里已写了很多,原来只需三句代码都可以写成这样,更把乱数的分布变得更不平均,不再多说了。

算法实现

本来以为《用》一文中的说法,已足够解释。以上的情况,加上本人未试过实现别名方法(alias method),便做了二个实验去比较及分析各种算法。实验有其局限性,只是在某个客观环境(计算机、编译器等),对某个算法实现的测量。但本实验的结果可能可以支持我在《用》所提及的观点。

逆变换方法(线性搜寻)

这个算法于《用》有详细说明,以下是其C++代码,复杂度是O(N):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template  < typename  T, typename  RandomGenerator>
class  InverseLinear {
public :
     InverseLinear( const  T* cdf, size_t  N, RandomGenerator& random) : mCDF(cdf), mN(N), mRandom(random) {
         assert (N > 0);
         assert (cdf[N - 1] == (T)1.0);
     }
 
     size_t  operator()() {
         T y = mRandom();
         for  ( size_t  x = 0; x < mN - 1; x++)
             if  (y < mCDF[x])
                 return  x;
 
         return  mN - 1;
     }
 
     size_t  GetMemory() const  {
         return  sizeof (* this ) + mN * sizeof (T);
     }
 
private :
     const  T* mCDF;
     const  size_t  mN;
     RandomGenerator mRandom;
};

逆变换方法(二元搜寻)

这个算法用二元搜寻代替线性搜寻,复杂度降低为O(lg N):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
template  < typename  T, typename  RandomGenerator>
class  InverseBinary {
public :
     InverseBinary( const  T* cdf, size_t  N, RandomGenerator& random) : mCDF(cdf), mN(N), mRandom(random) {
         assert (N > 0);
         assert (cdf[N - 1] == (T)1.0);
     }
 
     size_t  operator()() {
         T y = mRandom();
         size_t  left = 0, right = mN;
 
         while  (left < right - 1) {
             size_t  mid = (left + right) / 2;
             assert (mid - 1 < mN);
             if  (mCDF[mid - 1] < y)
                 left = mid;
             else
                 right = mid;
         }
 
         return  left;
     }
 
     size_t  GetMemory() const  {
         return  sizeof (* this ) + mN * sizeof (T);
     }
 
private :
     const  T* mCDF;
     const  size_t  mN;
     RandomGenerator mRandom;
};

必须注意这个算法和一般的二元搜寻条件有些差异。

别名方法

别名方法能提供O(1)的复杂度,但初始化过程比较复杂。参照[1]附录的代码实作:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
template  < typename  T, typename  RandomGenerator>
class  Alias {
public :
     Alias( const  T* pdf, size_t  N, RandomGenerator& random) : mN((T)N), mRandom(random), mAliasTable(N) {
         assert (N > 0);
         std::vector< size_t > smallBag, largeBag;
         smallBag.reserve(N);
         largeBag.reserve(N);
 
         const  T stdWeight = (T)1 / N;
         for  ( size_t  i = 0; i < N; i++) {
             mAliasTable[i].prob = pdf[i];
 
             if  (pdf[i] > stdWeight)
                 largeBag.push_back(i);
             else
                 smallBag.push_back(i);
         }
 
         while  (!largeBag.empty() && !smallBag.empty()) {
             const  size_t  sm = smallBag.back();
             const  size_t  lg = largeBag.back();
             smallBag.pop_back();
             largeBag.pop_back();
 
             mAliasTable[lg].prob -= stdWeight - mAliasTable[sm].prob;
             mAliasTable[sm].prob *= (T)N;
             mAliasTable[sm].index = lg;
 
             if  (mAliasTable[lg].prob > stdWeight)
                 largeBag.push_back(lg);
             else
                 smallBag.push_back(lg);
         }
 
         for  (std::vector< size_t >::iterator itr = smallBag.begin(); itr != smallBag.end(); ++itr)
             mAliasTable[*itr].prob = (T)1;
 
         for  (std::vector< size_t >::iterator itr = largeBag.begin(); itr != largeBag.end(); ++itr)
             mAliasTable[*itr].prob = (T)1;
     }
 
     __forceinline size_t  operator()() {
         T u = mRandom() * mN;
         size_t  i = ( size_t )u;
         return  u - i > mAliasTable[i].prob ? mAliasTable[i].index : i;
     }
 
     size_t  GetMemory() const  {
         return  sizeof (* this ) + sizeof (AliasItem) * mAliasTable.capacity();
     }
 
private :
     T mN;
     RandomGenerator mRandom;
 
     struct  AliasItem {
         T prob;
         size_t  index;
     };
     std::vector<AliasItem> mAliasTable;
};

伪随机数产生器

Visual C++的C运行时库(C runtime library, CRT)中,提供的rand()是线程安全的,因而有额外的开销(需要存取TLS)。而且,其精确度有限(只有15-bit)。因此,在实验中使用了两个伪乱数产生器,一个采用rand(),一个采用最普通的线性同余产生器(Linear congruential generator, LCG)。两个产生器都是产生半合区间[0,1)的匀均分布。

?
1
2
3
4
5
6
7
8
9
10
11
template  < typename  T>
class  RandomCRT {
public :
     RandomCRT(unsigned seed = 0) {
         srand (seed);
     }
 
     T operator()() {
         return  rand () * ((T)1 / (RAND_MAX + 1));
     }
};
?
1
2
3
4
5
6
7
8
9
10
11
12
13
template < typename  T>
class  RandomLCG {
public :
     RandomLCG(unsigned seed = 0) : mSeed(seed) {}
 
     T operator()() {
         mSeed = 214013 * mSeed + 2531011;
         return  mSeed * ((T)1 / 4294967296);
     }
     
private :
     unsigned mSeed;
};

实验一: 方法

主要的测试方式如下。设N为4, 8, 16, ..., 256,用乱数产生一组N个元素的pdf,之后对每个算法,执行其初始化1,000次,采样1,000,000次,分别量度时间。此外,用采样来计算频表,之后用以下算式比较频表和pdf的总误差:

?
1
2
3
4
5
6
7
8
9
template  < typename  T>
T ComputeError( const  unsigned* histogram, const  T* pdf, size_t  N, size_t  sampleCount) {
     T error = 0.0;
     for  ( size_t  i = 0; i < N; i++) {
         T delta = (T)histogram[i] / sampleCount - pdf[i];
         error += abs (delta);
     }
     return  error;
}

当中是采样等于i的出现的次数除以总采样次数。

有些算法分别采用float和double两种类型,亦会使用CRT和LCG两种伪乱数产生器,所有测试组合如下:

  • Tony
  • Linear<CRT, double>
  • Binary<CRT, double>
  • Alias<CRT, double>
  • Linear<LCG, double>
  • Binary<LCG, double>
  • Alias<LCG, double>
  • Linear<LCG, float>
  • Binary<LCG, float>
  • Alias<LCG, float>

实验环境:

  • Intel i7 920 @2.67Hz
  • Microsoft Windows 7 64-bit

编译器及主要编译参数:

  • Microsoft Visual Studio 2008 9.0.30729.1 SP
  • Platform: Win32
  • Optimization: Maximize Speed (/O2)
  • Inline Function Expansion: Any Suitable (/Ob2)
  • Enable Intrinsic Function: Yes (/Oi)
  • Favor Size of Speed: Favor Fast Code (/Ot)
  • Enable C++ Exception (No)
  • Runtime Library: Multi-threaded DLL (/MD)
  • Buffer Security Check: No (/GS-)
  • Enable Enhanced Instruction Set: Streaming SIMD Extensions 2 (/arch:SSE2)
  • Floating Point Model: Fast (/fp:fast)
  • Default Char Unsigned: Yes (/J)
  • Enable Run-Time Type Info: No (/GR-)

宏:

  • #define _SECURE_SCL 0

实验一: 结果及分析

以下结果皆使用Google Visualization API显示,因此在RSS或转载中可能无法显示。我初次试用这API,其HTML和JavaScript代码是由C++测试程序中自动生成的。

原始数据表如下(init和sample的单位为秒,memory的单位为字节):

sampler N init sample error memory
Tony 4 0.000664 0.027172 0.081564 11008
Linear<CRT, double> 4 0 0.036899 0.001122 44
Binary<CRT, double> 4 0 0.037687 0.000393 44
Alias<CRT, double> 4 0.000901 0.039043 0.001285 96
Linear<LCG, double> 4 0 0.012864 0.001358 44
Binary<LCG, double> 4 0 0.015508 0.001358 44
Alias<LCG, double> 4 0.000901 0.020245 0.001269 96
Linear<LCG, float> 4 0 0.014556 0.001358 28
Binary<LCG, float> 4 0 0.013205 0.001358 28
Alias<LCG, float> 4 0.000388 0.019169 0.001269 56
Tony 8 0.000866 0.02719 0.114413 11008
Linear<CRT, double> 8 0 0.041352 0.001867 76
Binary<CRT, double> 8 0 0.044668 0.001192 76
Alias<CRT, double> 8 0.000613 0.040483 0.002028 160
Linear<LCG, double> 8 0 0.021303 0.001793 76
Binary<LCG, double> 8 0 0.024576 0.001793 76
Alias<LCG, double> 8 0.000605 0.01771 0.001696 160
Linear<LCG, float> 8 0 0.02296 0.001793 44
Binary<LCG, float> 8 0 0.020713 0.001793 44
Alias<LCG, float> 8 0.000626 0.016415 0.001696 88
Tony 16 0.001118 0.02716 0.100027 11008
Linear<CRT, double> 16 0 0.046342 0.002474 140
Binary<CRT, double> 16 0 0.049653 0.001773 140
Alias<CRT, double> 16 0.000965 0.040003 0.003115 288
Linear<LCG, double> 16 0 0.026674 0.002771 140
Binary<LCG, double> 16 0 0.033027 0.002771 140
Alias<LCG, double> 16 0.000772 0.019638 0.002637 288
Linear<LCG, float> 16 0 0.029094 0.002771 76
Binary<LCG, float> 16 0 0.02711 0.002771 76
Alias<LCG, float> 16 0.000757 0.018408 0.002637 152
Tony 32 0.00168 0.027125 0.117478 11008
Linear<CRT, double> 32 0 0.054437 0.003796 268
Binary<CRT, double> 32 0 0.060345 0.003682 268
Alias<CRT, double> 32 0.001328 0.04101 0.00407 544
Linear<LCG, double> 32 0 0.0378 0.004307 268
Binary<LCG, double> 32 0 0.038143 0.004307 268
Alias<LCG, double> 32 0.001088 0.021245 0.004277 544
Linear<LCG, float> 32 0 0.03846 0.004307 140
Binary<LCG, float> 32 0 0.039474 0.004307 140
Alias<LCG, float> 32 0.001063 0.020093 0.004277 280
Tony 64 0.002716 0.027327 0.125566 11008
Linear<CRT, double> 64 0 0.068234 0.006185 524
Binary<CRT, double> 64 0 0.071522 0.005066 524
Alias<CRT, double> 64 0.002016 0.041144 0.005638 1056
Linear<LCG, double> 64 0 0.051763 0.005387 524
Binary<LCG, double> 64 0 0.048 0.005387 524
Alias<LCG, double> 64 0.001813 0.021425 0.004949 1056
Linear<LCG, float> 64 0 0.052001 0.005387 268
Binary<LCG, float> 64 0 0.049192 0.005387 268
Alias<LCG, float> 64 0.001906 0.020285 0.004949 536
Linear<CRT, double> 128 0 0.103351 0.008263 1036
Binary<CRT, double> 128 0 0.079406 0.007868 1036
Alias<CRT, double> 128 0.003739 0.04089 0.008274 2080
Linear<LCG, double> 128 0 0.087546 0.006949 1036
Binary<LCG, double> 128 0 0.05506 0.006949 1036
Alias<LCG, double> 128 0.003442 0.021405 0.006866 2080
Linear<LCG, float> 128 0 0.087284 0.006943 524
Binary<LCG, float> 128 0 0.056546 0.006943 524
Alias<LCG, float> 128 0.003467 0.020354 0.006866 1048
Linear<CRT, double> 256 0 0.172883 0.011585 2060
Binary<CRT, double> 256 0 0.087077 0.011734 2060
Alias<CRT, double> 256 0.006625 0.041404 0.011714 4128
Linear<LCG, double> 256 0 0.156308 0.010972 2060
Binary<LCG, double> 256 0 0.063352 0.010972 2060
Alias<LCG, double> 256 0.006188 0.021585 0.011057 4128
Linear<LCG, float> 256 0 0.157396 0.010976 1036
Binary<LCG, float> 256 0 0.064066 0.010968 1036
Alias<LCG, float> 256 0.006339 0.020347 0.011057 2072

初始化时间

纵轴是时间,单位为秒。注意横轴为对数比例。

Initialization TimeTonyLinear<CRT,double>Binary<CRT,double>Alias<CRT,double>Linear<LCG,double>Binary<LCG,double>Alias<LCG,double>Linear<LCG,float>Binary<LCG,float>Alias<LCG,float>481632641282560.0000.0020.0040.0060.008

除了Tony和Alias,其余算法的初始化都接近零。 Tony和Alias都是以线性上升。大部份情况下,Tony的初始化性能较低。

采样时间

Sampling TimeTonyLinear<CRT,double>Binary<CRT,double>Alias<CRT,double>Linear<LCG,double>Binary<LCG,double>Alias<LCG,double>Linear<LCG,float>Binary<LCG,float>Alias<LCG,float>481632641282560.000.050.100.150.20

最好成绩的是Alias的LCG版本,接近常数性能,其float和double版本差异不大。 Tony亦为常数性能,比Alias慢30%左右。

一如所料,Linear和Binary的性能为O(N)和O(lg N)(因横轴为对数比例,对数在图里会接近直线,例如橙线的Binary)。值得注意的是,Linear和Binary的LCG版本在N=4时,超越Alias和Tony。这是由于这两个算法简单,系数低,所以有机会超越O(1)的对手。

需要较高性能的话,别用CRT的rand()。

准确度

ErrorTonyLinear<CRT,double>Binary<CRT,double>Alias<CRT,double>Linear<LCG,double>Binary<LCG,double>Alias<LCG,double>Linear<LCG,float>Binary<LCG,float>Alias<LCG,float>481632641282560.000.040.080.120.16

如果要说Tony算法的主要问题,就在于其准确度远远差于其他算法的实现(在数百倍以上)。就算使用整数的百份比,用那N=5的例子,Tony的误差约为0.08,其他算法则介乎0.001至0.002。

内存用量

MemoryTonyLinear<CRT,double>Binary<CRT,double>Alias<CRT,double>Linear<LCG,double>Binary<LCG,double>Alias<LCG,double>Linear<LCG,float>Binary<LCG,float>Alias<LCG,float>4816326412825603,0006,0009,00012,000

Tony算法所使用的内存是常量11008个字节。而其他的算法为O(N),Alias因为要多存一个别名索引,其内存是其他除Tony以外的两倍(事实上,Alias的索引可以因N的大小而采用unsigned short或unsigned char,以减低开销)。

这里亦要留意一点,Linear和Binary其实只储存cdf的常数指针、N和Random,实际上只有12字节。 cdf数组可供多个采样器共用,所以Linear和Binary的内存开销是非常少的。

N的范围

Tony算法的另一缺点是只支持至N=100。相反,其他的算法可支持较大的N,例如用Tony所需的內存,Alias的float版本可支持N=1375。

实验二: 方法

上一个实验,量度各个算法的采样器在不同的N时的性能。但是在应用上,很多时候需要同时使用多个采样器。肖舸在《实》中提及:

实话跟你讲吧,这段代码是有前提的,我们要做5000万条记录,中间有20万个设备的记录,每个设备的采样频率不一样,我要并发模拟,你再想想我写这么复杂有道理没?

那么就实际测试一下。建立M个采样器(M设为1、10、...、100000),对每个采样器轮流采样,M个采样器合共采样1百万次。而N则取Tony算法可接受的最大值100。测试的循环代码是这样的:

?
1
2
3
4
5
6
7
8
9
10
const  size_t  interleavedSample = sampleCount / M;
 
size_t  unused = 0; // prevent optimized out
LARGE_INTEGER start, end;
QueryPerformanceCounter(&start);
for  ( size_t  i = 0; i < interleavedSample; i++)
     for  ( size_t  j = 0; j < M; j++)
         unused += (*samplers[j])();
QueryPerformanceCounter(&end);
cout << unused << endl;

从理论上来说,用一个采样器去做一百万次采样,和用M个采样器合共采样一百万次,在效能上应该没有分别。但在实际的测试中又是否这样呢?

你可能感兴趣的:(实验比较各离散采样算法)