c++ 11,随机分布,和迅速

概述

从随机分布中生成数字是一种实用的工具,任何编码器都可能在某个时候需要它。c++ 11增加了丰富的随机分布生成功能。这使得使用随机分布变得简单快捷,不仅仅是你在用c++ 11,但是如果你用的是任何一种可以和c++交互的语言。

在这篇文章中,我们会学到随机分布的用处,它们是如何产生的,以及如何在c++ 11中使用它们。我还将展示如何通过将c++ 11的类包装成Swift类来为Swift创建新的随机分发功能。虽然Swift不直接支持c++,我将展示如何通过为c++类创建纯C包装器来解决这个问题。

随机分布,为什么它们很重要

如果名字像负二项泊松只是对很久以前学过的东西的回忆的影子,请给我一点时间来说服你们,随机分布的世界值得你们花时间和精力。

编码器已经意识到随机数的概念。但对许多人来说,我们的工具箱仅限于统一的实数和整数随机数,也许偶尔也会抛出一些高斯(正态)随机数。产生随机数的方法还有很多!事实上,您甚至可能发现自己在不知情的情况下重新创建了标准的随机分布……

例如,假设你在写一个音乐播放器,你的用户已经给不同的歌曲打分,从一星到五星。你想要实现一个洗牌播放功能,它会随机选择歌曲,但是经常选择高评级的歌曲。你将如何实现它?manbetx官网手机登录答案是:随机分布!更具体地说,你想从a中得到随机数离散分布;也就是说,生成一个随机整数,使用一组加权数,其中加权数越高,按比例选择的次数就越多。

或者,在向系统添加更多资源之后,您可能正在尝试模拟预测的队列长度。你模拟这个过程,因为你想知道的不仅仅是平均队列长度,但是有多少次它会大于某个大小,95%的大小是多少,等等。你不确定系统的一些输入是什么,但是你知道可能值的范围,你猜一下你认为什么是最有可能的。在这种情况下,你想从a中得到随机数三角形分布;也就是说,生成一个随机浮动,这通常很接近你的猜测,距离越远,线性概率越小,在可能值范围之外的概率为零。(这种模拟形成了概率的编程)。

还有很多其他随机分布,包括:

如何生成随机分布

一般来说,从随机分布中生成数字的步骤如下:

  1. 种子你的发电机
  2. 使用生成器生成下一串随机比特
  3. 将这些随机位转换为您选择的分布
  4. 如果你需要更多的随机数,返回(2)

我们通常所说的“随机数生成”实际上是步骤(2):使用a伪随机信号发生器它确定性地生成尽可能“随机”的一系列数字。彼此不相关,分散,等等)。伪随机发生器是具有这些性质的函数,如梅森素数捻线机。首先,你需要一些种子;也就是说,传递给生成器的第一个数字。大多数操作系统都有生成随机种子的方法,如/dev/random在Linux和Mac上,它使用来自设备驱动程序的噪声等环境输入来获得一个应该是真正随机的数字。

然后在步骤3中,我们将伪随机生成器生成的随机比特转换成我们需要的分布。有一些普遍适用的方法,如逆变换抽样,将一个均匀的随机数转换成任意给定的分布。还有一些针对某个发行版的更快的方法,如Box-Muller变换它从均匀生成器中创建高斯(正态)随机数。

为了得到更多的随机数,你不需要回去/dev/random,因为现在已经设置了一个伪随机生成器。相反,只需从生成器中获取下一个数字(步骤(2)),然后将其传递给生成转换的分布(步骤(3))。

这在c++中是如何工作的

c++ 11包括,在 标准库头,上述每个步骤的功能。步骤(1)通过简单地创建a来实现random_device)我不包括std::本文中的前缀;你可以输入std::random_device或添加使用名称空间性病到您的c++文件的顶部)。然后将其传递给所提供的各种伪随机生成器的构造函数,如mt19937,即梅森捻线发电机;步骤(2)。然后使用适当的类构造一个随机分布对象,如discrete_distribution,传入该发行版所需的任何参数(例如为discrete_distribution传入每个可能值的权重列表)。最后,调用该对象(它支持()运营商,这是一个函子,被称为可调用的传入您创建的伪随机生成器。这是一个完整的示例来自优秀的cppreference.com。

一个来自cppreference.com的c++离散分布()的例子
一个来自cppreference.com的c++离散分布()的例子

如果你认为C++代码必须冗长而复杂,这个例子可能会让你重新思考你的假设!如你所见,每个步骤都很好地映射到上一节中描述的随机分布过程的概述。(顺便说一句:如果你有兴趣学习现代c++,cppreference.com为c++标准库的每个部分都精心设计了非凡的示例集;这也许是学习如何有效地使用语言的最好的地方。您甚至可以编辑示例并在线运行它们!

c++ 11提供的发行版有:

这在Swift中是如何工作的

虽然Swift 4现在提供了一些基本的随机数支持,它仍然没有提供任何非均匀分布。因此,我已经为Swift提供了所有c++ 11随机发行版,作为我的巴实抹图书馆。有关我创建这个库的原因和方法的更多信息,看到高性能数字编程与Swift。稍后我将展示如何构建这个包装器,但首先让我们看看如何使用它。这是我们在c++中看到的同一个函数,迅速转化为+巴:

 加勒比海盗 = Intdiscrete_distribution([40, 10, 10, 40))(10000]
 计数 = 加勒比海盗减少(: [:]) { 0美元(1美元, 默认的:0] + = 1 }
计数排序(通过:<)forEach { 打印(\(0美元)生成的\(1美元)次”) }

如你所见,Swift中随机数的生成可以用BaseMath归结为:Int.discrete_distribution([40岁10日,10日,40])[10000]。我们可以比c++ 11更简洁地做到这一点,因为我们没有提供太多的选项和细节。BaseMath只是假设你想使用标准的播种方法,并使用mt19937梅森素数捻线机发电机。

BaseMath中的分布名称与c++ 11中的完全相同,您只需在每个名称前面加上希望生成的类型(两者都可以)IntInt32为整型分布,或浮动真正的分布)。每个分布都有一个初始化它与c++ 11分发构造函数匹配相同的名称和类型。这将返回一个带有许多方法的Swift对象。11 c++对象,如前所述,提供()(函子)操作符,但不幸的是,该操作符不能在Swift中重载。因此我们借用Swift的下标给出等价行为的特殊方法。唯一的区别是我们必须使用[]而不是()。如果只使用空下标[]然后BaseMath将返回一个随机数;如果你使用Int,如[10000]然后BaseMath将返回一个数组。(也有生成缓冲区指针和对齐存储的方法。)使用下标不是一个函子一开始会觉得有点奇怪,但这是一个完美的方式,以绕过斯威夫特的限制。我称之为aquasi-functor;也就是说,像函子一样的东西,但是叫做使用[…]

用Swift包装c++

为了使Swift可以使用c++ 11随机发行版,我需要做两件事:

  1. 将c++类包装在一个C API中,因为Swift不能直接与c++交互
  2. 用Swift类包装C API

我们将依次研究每一步:

用C语言包装c++类

C包装器代码在里面CBaseMath.cpp。的mt19937mersenne twister generator将被包装在一个名为Swift的类中RandGen,包装这个类的C函数都有RandGen_前缀。下面是C包装器的代码(注意包装器可以在内部使用c++特性,只要头文件中的接口是纯C):

结构体 RandGenC:mt19937 { }; 
类型定义 结构体 RandGenC RandGenC;
RandGenC* RandGen_create() {
  返回 (RandGenC*)( mt19937(random_device()());
}
无效 RandGen_destroy(RandGenC* v) {
  删除(v);
}

我们包装的每个类的模式与此类似。我们至少会有:

  1. 一个结构体这是从C++类派生出来的struct RandGenC),以及允许我们直接使用此名称的类型定义。使用struct而不是void *我们可以直接调用c++中的方法,但可以隐藏模板和其他c++内部从我们的纯C头文件
  2. 一个_create函数的作用是:构造该类的对象并返回指向该对象的指针,转换为结构类型
  3. 一个_destroy函数的作用是:删除该对象

在我们的头,我们会列出每个函数,以及类型定义。任何导入这个C API的东西,包括我们的Swift代码,实际上不知道struct包含什么,manbetx官网手机登录因此我们不能直接使用类型(因为header中没有提供它的大小和布局)。相反,我们将在使用它的代码中使用不透明指针。

下面是一个发行版的包装器代码;看起来几乎一样:

结构体 uniform_int_distribution_intC:uniform_int_distribution<int> { };
类型定义 结构体 uniform_int_distribution_intC uniform_int_distribution_intC;
uniform_int_distribution_intC* uniform_int_distribution_int_create(int 一个,int ) {
  返回 (uniform_int_distribution_intC*)( uniform_int_distribution<int>(一个,));
}
无效 uniform_int_distribution_int_destroy(uniform_int_distribution_intC* v) {
  删除(v);
}
int uniform_int_distribution_int_call(uniform_int_distribution_intC* p, RandGenC* g) {
  返回 (*p)(*g);
}

主要的区别是a的加法_call函数来允许我们实际调用该方法。同时,因为类型是模板化的,我们必须为要支持的每种模板类型创建一组单独的包装器;上面给出了一个例子 。注意,该类型需要包含在每个函数的名称中,因为C不支持重载。

当然,这些看起来都很冗长,我们不需要对每个分布都写出来。所以我们不!相反,我们使用gyb为我们创建它们的模板,还可以自动生成头文件。如果时间允许,我们以后会更详细地讨论这个问题。但是现在,您可以检查模板的源代码

用Swift封装C API

现在我们有了C API,我们可以在Swift中轻松地重新创建原始c++类,例如:

公共  RandGen {
  公共  ptr:OpaquePointer吗?
  公共 初始化() { ptr=RandGen_create() }
  deinit { RandGen_destroy(ptr) }
}

如你所见,我们只需调用_create函数初始化,和_destroydeinit。如前一节所讨论的,我们的C API用户对结构的内部结构一无所知,manbetx官网手机登录斯威夫特只是给了我们一个OpaquePointer

我们为每个发行版创建类似的包装器(它还定义下标,它会调用我们的_call功能),加上使用适当的静态包装器扩展数值类型,例如:

扩展 Int32 {
  公共 静态 函数 uniform_int_distribution(_ g_:RandGen, _ 一个:Int32, _ :Int32)
      - > uniform_int_distribution_Int32 {
    返回 uniform_int_distribution_Int32(g_, 一个,)
  }
}

线程安全的发电机

必须构造并传入aRandGen对象不方便,特别是当我们必须处理线程安全的复杂性时。c++库不是,一般来说,线程安全;这包括c++ 11随机生成器。所以我们必须小心,不要在线程之间共享一个RandGen对象。如前所述高性能数字编程与Swift文章中,通过使用,我们可以很容易地获得线程安全的对象线程本地存储。我添加了这个属性RandGen:

静态 var 存储:RandGen {
  如果  r = 线程当前的threadDictionary(“RandGen”] 是吗? RandGen { 返回 r }
  返回 线程setToTLS(RandGen(), “RandGen”)
}

这样就可以向每个发行版类添加以下版本,这使用户永远不必考虑创建或使用manbetx官网手机登录RandGen类。

公共 方便 初始化(_ 一个:Int32, _ :Int32) { 
  自我初始化(RandGen存储, 一个,)
} 

使用协议和BaseMath的扩展

以上步骤提供了一次生成单个随机数的功能。为了生成集合,我们可以添加一个分布每个发行版都遵守的协议,并将其扩展如下:

公共 协议 分布:Nullary {
  下标()- >元素 {得到}
  下标(n:Int)- >(元素] {得到}
}
扩展 分布 {
  公共 下标(n:Int)- >(元素] {
    返回 (元素]填满(自我, n)
  }
}

如你所见,我们利用BaseMath方法填满,哪个调用函数或准函子n然后返回一个新的BaseVector(在这种情况下,一个数组)和每次通话的结果。

你可能想知道协议manbetx官网手机登录Nullary这是上面提到的。也许你已经听说过一元(只有一个参数的函数或运算符)二进制(两个参数),和三元);不太清楚,但同样有用,是这个词nullary,它只是一个没有参数的函数或运算符。正如前面所讨论的,Swift不支持重载()运营商,所以我们加上aNullary协议使用下标:

公共 协议 Nullary {
  associatedtype 元素:SignedNumeric
  下标()- >元素 {得到}
}

试一下!

如果你是c++或Swift程序员,尝试一些随机分布—也许您甚至可以尝试创建一些模拟并进入概率编程的世界!或者如果你是一个敏捷的程序员,想在c++库中使用函数,尝试用一个惯用的Swift API来包装它,并将其作为Swift包提供给任何人使用。