65.9K
CodeProject 正在变化。 阅读更多。
Home

SFMT 实战: 第一部分 - 生成包含 SSE2 支持的 DLL

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (4投票s)

2008 年 12 月 2 日

CPOL

22分钟阅读

viewsIcon

73323

downloadIcon

773

一种使用 SFMT(SIMD-oriented Fast Mersenne Twister)随机数生成器算法的方法。

目录

引言

如您所知,许多软件开发人员在开发应用程序时需要随机数。特别是,金融和基于估计的应用程序是随机数的常用领域。如今,有许多随机数生成器,其中一些是开源且免费使用的。MT (Mersenne Twister) 及其改进版本 SFMT (SIMD-oriented Fast Mersenne Twister) 都是非常流行且众所周知的随机数生成器算法。

方法

“SFMT 实战”系列的第一部分是关于生成一个面向 SIMD 的快速 Mersenne Twister DLL。这个 DLL 将能够利用 CPU 的能力,例如 SSE2。

流式 SIMD 扩展 2:SSE2

SSE2,流式 SIMD 扩展 2,是 IA-32 SIMD(单指令多数据)指令集之一。SSE2 最初由 Intel 在 2001 年的 Pentium 4 初版中引入。它扩展了早期的 SSE 指令集,旨在完全取代 MMX。Intel 在 2004 年扩展了 SSE2 以创建 SSE3。SSE2 在 SSE(70 条指令)的基础上增加了 144 条新指令。竞争对手芯片制造商 AMD 在 2003 年引入其 Opteron 和 Athlon 64 系列 AMD64 64 位 CPU 时添加了对 SSE2 的支持。

当应用程序被设计为利用 SSE2 并在支持 SSE2 的机器上运行时,它们几乎总是比以前更快。如今,许多 CPU 都支持 SSE2 指令集。有关 SSE2 的详细信息,请访问此链接

关于 SFMT

在开始生成 SFMT DLL 之前,让我们先谈谈它。

面向 SIMD 的快速 Mersenne Twister (SFMT) 是一种线性反馈移位寄存器 (LFSR) 生成器,它一步生成一个 128 位伪随机整数。它由(广岛大学的)Mutsuo SaitoMakoto Matsumoto 于 2006 年引入。SFMT 的设计考虑了现代 CPU 的最新并行性,例如多级流水线和 SIMD(例如,128 位整数)指令。它支持 32 位和 64 位整数,以及双精度浮点输出。SFMT 是 Mersenne Twister (MT) 的变体,速度是其两倍。因此,SFMT DLL 可以生成 32 位和 64 位整数,这很好。

您可以在此处找到 SFMT 的官方网站。

理论 - SFMT 的数学模型

SFMT 结构的学术概念的详细解释可在此处找到。

让我们开始

正如我之前所说,在本文中,我将尝试生成一个 SFMT DLL,并且在这样做时,我将使用 SFMT 代码的原始版本。其原始 C 实现(版本 1.3.3)可以从此处下载。在我的开发过程中,将逐步解释对原始 C 实现的一些特殊和必要的更改以及修改原始代码的原因。生成 SFMT.dll 的基本概念不是更改或修改其核心代码,而是使这些代码可以从生成的 DLL 外部调用和使用。

请注意,我将在 Windows Vista 上使用 Visual Studio 2008;既用于分析原始代码也用于开发 SFMT DLL。

在 Visual Studio 中,我启动了一个名为 SFMT 的新 C++ Win32 项目

Sample Image

现在,将显示 Win32 应用程序向导。在此窗口中,从“应用程序设置”中,我为“应用程序类型”选择“DLL”,并为“附加选项”勾选“空项目”。

Sample Image

点击“完成”按钮后,将在 Visual Studio 屏幕上创建一个新的空项目。

我将从此地址下载的 SFMT 原始 C 实现代码解压缩到 Visual Studio 2008\Projects\SFMT 目录下。解压缩后,您会看到许多文件,但请务必注意我们不会使用所有文件。其中一些用于测试目的,有些文件包含测试结果。

实际上,在 C 实现(版本 1.3.3)中,我关注了五个主要代码文件,它们是:

  1. SFMT.c:SFMT 生成器引擎的核心代码位于此文件中。它实现了主要方法,例如用于生成 32 位整数的“gen_rand32”方法。
  2. SFMT.h:通过此文件,可以轻松调用主要方法。此外,此处还实现了其他有用的方法,例如生成实数。
  3. SFMT-params.h:它包含一些基本定义,例如 MEXP 和用于生成伪随机数的参数。此外,此处还编写了用于当前 MEXP(梅森指数)和“include”结构的一些预处理器规则。
  4. #elif MEXP == 19937 #include "SFMT-params19937.h"
  5. SFMT-sse2.h:它提供 SSE2 支持,当然,它可以通过 CPU 的 SSE2 指令访问加速代码。它使用 emmintrin.h
  6. SFMT-paramsXXXXXX.h:其他必要的参数位于这些文件中。在这里,XXXXXX 代表 MEXP 常量。有十个参数文件,它们针对不同的 MEXP 值进行了配置。MEXP 及其含义在下一段中提及。

在代码中,您会看到一个名为 MEXP 的定义,它是使用该算法的起点。MEXP 的意思是 梅森指数。生成的代码的周期将是 2MEXP-1。它是使用该算法的必要定义。它必须是这些值之一:60712792281425311213199374449786243132049216091

除非您未指定,否则默认值为 19937。

如果您检查 SFMT 的原始实现,您会发现它可以在三种可能的平台上编译:

  1. 没有 SIMD 指令的标准 C
  2. 支持 Intel SSE2 指令的 CPU + 支持这些功能的 C 编译器
  3. 支持 PowerPC Altivec 指令的 CPU + 支持这些功能的 C 编译器

如上所示,第 3 条不适用于基于 Microsoft 的平台,因为它使用 Altivec 指令。第 2 条(使用 SSE2 指令的力量)是我的选择。在生成 DLL 时,我的目标是修改代码以使用 SSE2 指令进行编译。因此,首先,我将清理代码中一些不必要的部分。此外,在开发结束时,当我构建和编译 SFMT.dll 时,您可以轻松地在标准 C 和 SSE2 支持的版本之间切换。

将 SFMT.c 添加到项目并进行修改

在“解决方案资源管理器”中,在 SFMT 项目下,我将现有的 SFMT.c 文件添加到 Source Files 目录并打开它进行修改。

Sample Image

最初,我将 SFMT.c 文件中的一些预处理器代码分离出来。例如,它包含一些定义和含义,如下所示:

  • #if defined(HAVE_ALTIVEC):这是可选的。如果指定此宏,则将生成针对 Altivec 优化的代码。此宏会自动打开 BIG_ENDIAN64 宏。
  • #if defined(BIG_ENDIAN64):当您的 CPU 是大端字节序且您正在使用 64 位输出时,此宏是必需的。因此,它适用于运行 Macintosh 操作系统的基于 PowerPC 的计算机。
  • #if defined(ONLY64):此宏是可选的。如果指定此宏,将生成用于大端 CPU 64 位输出的优化代码,并且不会生成 32 位输出的代码。
  • HAVE_ALTIVECBIG_ENDIAN64ONLY64 预处理器命令及其相关代码不适用于 Windows 平台,我已小心地从 SFMT.c 文件中删除这些命令及其相关代码。

    另一方面,有一个名为 HAVE_SSE2 的预处理器定义,它对我们来说至关重要。在删除其他不必要的定义时,保留 HAVE_SSE2 及其相关代码在文件中很重要。

  • #if defined(HAVE_SSE2):如果指定此宏,将生成针对 SSE2 优化的代码。
  •  

    32 位输出

    小端 64 位输出

    大端 64 位输出

    required

    MEXP

    MEXP

    MEXP, BIG_ENDIAN64

    optional

    HAVE_SSE2, HAVE_ALTIVEC

    HAVE_SSE2

    HAVE_ALTIVEC, ONLY64

SFMT.c 文件中,有两个函数用于用 32 位或 64 位随机整数填充数组。第一个是 fill_array32,第二个是 fill_array64。我修改了这些函数的一部分,并希望在此处提及这些更改:

  • 更改 fill_array32 和 fill_array64 的返回类型:在 SFMT 的原始 C 实现中,两个 fill_array 函数都没有返回值。这意味着它们使用 void 关键字。在我的 SFMT.dll 中,我将这些函数的返回类型升级为 int。之后,这些函数能够返回 0 或 1 值。如果函数返回 0,则表示数组未成功由函数填充。这几乎总是表明,某些用于进程的内存分配已失败。如果函数返回 1,则表示数组已成功由函数填充,并且数组已准备好使用。
  • 始终使用扩展大小数组以实现兼容性和灵活性:在 SFMT 的原始 C 实现中,对于 fill_array32fill_array64 函数,有两个规则:
    1. 对于 fill_array32,数组大小必须大于或等于 (MEXP / 128 + 1) * 4;对于 fill_array64,数组大小必须大于或等于 (MEXP / 128 + 1) * 2

    2. 对于 fill_array32,数组大小必须是 4 的倍数;对于 fill_array64,数组大小必须是 2 的倍数

由于这些规则,在生成伪随机数时,我不得不使用扩展大小的数组。此外,能够使用所有大小的数组是非常重要且灵活的。为了填充数组,我编写了新函数并将它们添加到 SFMT.c 代码文件。这些函数如下所示:

int get_array32_extended_size(int size)

/**
* This function is used to determine extended size of specified array[]
* in the fill_array32 function.

* Because, array size must be greater than or equal to (MEXP / 128 + 1) * 4
* so, let's fulfill the array if the size smaller than (MEXP / 128 + 1) * 4

* Because, array size must be a multiple of 4.
* so, let's fulfill the array.
*/

int get_array32_extended_size(int size) {
    int extended_size = 0;
    int remainder = 0;

    if (size < get_min_array_size32())
        extended_size = get_min_array_size32();
    else
        extended_size = size;

    remainder = extended_size % 4;
    extended_size = extended_size + 4 - remainder;

    return extended_size;
}

int get_array64_extended_size(int size)

/**
* This function is used to determine extended size of specified array[]
* in the fill_array64 function.

* Because, array size must be greater than or equal to (MEXP / 128 + 1) * 2
* so, let's fulfill the array if the size smaller than (MEXP / 128 + 1) * 2

* Because, array size must be a multiple of 2.
* so, let's fulfill the array.
*/

int get_array64_extended_size(int size) {
    int extended_size = 0;
    int remainder = 0;

    if (size < get_min_array_size64())
        extended_size = get_min_array_size64();
    else
        extended_size = size;

    remainder = extended_size % 2;
    extended_size = extended_size + 2 - remainder;

    return extended_size;
}

正如我在上一段中提到的,这些修改非常重要。通过这些修改,我们消除了“数组大小必须是 4 的倍数或 2 的倍数”的规则,以及“数组大小必须大于或等于 (MEXP / 128 + 1) * 4 或 (MEXP / 128 + 1) * 2”的规则。更清楚地说,例如,如果您想生成 2113 个整数,您可以使用修改后的 fill_array32fill_array64 函数轻松完成。使用原始版本的 fill_array32fill_array64 函数,您无法生成总共 2113 个整数。因为 2113 不是 4 的倍数也不是 2 的倍数。

注意:与 get_array32_extended_sizeget_array64_extended_size 函数集成的修改后的 fill_array32fill_array64 函数的主体如下所述。

  • 数据对齐和使用对齐的内存块:要使用 fill_array 函数,在 SIMD 版本中,指向数组的指针必须是“对齐的”(即,必须是 16 的倍数),因为它引用的是 128 位整数的地址。在标准 C 版本中,指针是任意的。如果定义了 HAVE_SSE2 宏,那么它要求指向数组的指针必须使用 16 字节对齐的内存块来生成随机整数。因为 SSE(流式 SIMD 扩展)定义了 128 位(16 字节)打包数据类型(4 个 32 位浮点数据),如果数据的地址以 16 字节对齐(即可以被 16 整除),则可以提高数据访问效率。16 字节对齐是使用 SSE2 支持的必要条件。此外,未对齐的数据也会降低数据访问性能。您可以访问songho 页面IBM 的此页面以获取有关 SSE2 数据对齐和 16 字节对齐的更多信息。

在 MSVC CRT 中,可以使用 _aligned_malloc() 函数分配动态数组,并使用 _aligned_free() 释放。下面给出了在 fill_array32fill_array64 中使用的对齐内存分配代码。

int* ptr;

#if defined(HAVE_SSE2)
    ptr = (w128_t *) _aligned_malloc(sizeof(uint32_t) * extended_size, 16);
#else
    ptr = (w128_t *) _aligned_malloc(sizeof(uint32_t) * 
			extended_size, __alignof(uint32_t));
#endif

修改后的 fill_array32fill_array64 函数如下所示

int fill_array32(uint32_t *array, int size) {
    int* ptr;
    int extended_size = get_array32_extended_size(size);

    assert(initialized);
    assert(idx == N32);

    /* The pointer to the allocated memory must be \b "aligned"
     * (namely, must be a multiple of 16) in the SIMD version (HAVE_SSE2), since it
     * refers to the address of a 128-bit integer. In the standard C
     * version, the pointer is arbitrary.
     */
    #if defined(HAVE_SSE2)
        ptr = (w128_t *) _aligned_malloc(sizeof(uint32_t) * extended_size, 16);
    #else
        ptr = (w128_t *) _aligned_malloc(sizeof(uint32_t) * 
			extended_size, __alignof(uint32_t));
    #endif

    if (ptr == NULL)
        return 0;
    else {
        gen_rand_array(ptr, extended_size / 4);
        memcpy((w128_t *)array, ptr, sizeof(uint32_t) * size);
        idx = N32;
        _aligned_free(ptr);
    }
    return 1;
}
int fill_array64(uint64_t *array, int size) {
    int* ptr;
    int extended_size = get_array64_extended_size(size);

    assert(initialized);
    assert(idx == N32);

    /* The pointer to the allocated memory must be \b "aligned"
     * (namely, must be a multiple of 16) in the SIMD version (HAVE_SSE2), since it
     * refers to the address of a 128-bit integer. In the standard C
     * version, the pointer is arbitrary.
     */
    #if defined(HAVE_SSE2)
        ptr = (w128_t *) _aligned_malloc(sizeof(uint64_t) * extended_size, 16);
    #else
        ptr = (w128_t *) _aligned_malloc(sizeof(uint64_t) * 
			extended_size, __alignof(uint64_t));
    #endif

    if (ptr == NULL)
        return 0;
    else {
        gen_rand_array(ptr, extended_size / 2);
        memcpy((w128_t *)array, ptr, sizeof(uint64_t) * size);
        idx = N32;
        _aligned_free(ptr);
    }
    return 1;
}

修改 SFMT-params.h 文件

在这个文件中,我只删除了 #ifdef __GNUC__ 预处理器定义及其相关代码。因为我正在使用 Microsoft Visual Studio C++ 编译器来生成 DLL,所以不需要基于 GNU 的代码。

您可以在此文件中看到一些基本定义。它们的结构和含义如下所示:

/*-----------------
BASIC DEFINITIONS
-----------------*/
/** Mersenne Exponent. The period of the sequence
* is a multiple of 2^MEXP-1.
* #define MEXP 19937 */
/** SFMT generator has an internal state array of 128-bit integers,
* and N is its size. */
#define N (MEXP / 128 + 1)
/** N32 is the size of internal state array when regarded as an array
* of 32-bit integers.*/
#define N32 (N * 4)
/** N64 is the size of internal state array when regarded as an array
* of 64-bit integers.*/
#define N64 (N * 2)

此外,还包含了一些依赖于梅森指数的 #include 预处理器命令。代码结构如下所示:

#if MEXP == 607
  #include "SFMT-params607.h"
#elif MEXP == 1279
  #include "SFMT-params1279.h"
#elif MEXP == 2281
  #include "SFMT-params2281.h"
#elif MEXP == 4253
  #include "SFMT-params4253.h"
#elif MEXP == 11213
  #include "SFMT-params11213.h"
#elif MEXP == 19937
  #include "SFMT-params19937.h"
#elif MEXP == 44497
  #include "SFMT-params44497.h"
#elif MEXP == 86243
  #include "SFMT-params86243.h"
#elif MEXP == 132049
  #include "SFMT-params132049.h"
#elif MEXP == 216091
  #include "SFMT-params216091.h"
#else
#endif

MEXP 值用作确定并将正确参数文件包含到项目中的标准,通过此机制,开发人员只需更改 MEXP 的值即可使用他们所需的参数文件。由于此机制,原始 SFMT 实现涵盖了十个不同的 SFMT-paramsXXXXXX.h 头文件。

在我的项目中,我将 MEXP 设置为 19937。此外,19937 也是原始 C 实现的默认值。

SFMT-paramsXXXXXX.h 文件中的更改

修改完 SFMT-params.h 文件后,是时候修改相关的 SFMT-paramsXXXXXX.h 文件了。目前有十个文件,每个文件都有自己的描述。MEXP 常量可以取十个不同的值,因此目前有十个不同的 paramsXXXXXX.h 文件。我将 MEXP 设置为 19937,要修改的第一个文件是 SFMT-params19937.h

SFMT-params19937.h 头文件中,有一些用于 Altivec 的参数。它们在代码中以 #if defined (__APPLE__) 结构开头。我删除了这个预处理器代码块。这个块包含 MAC OS X 的参数,如下所示:

/* PARAMETERS FOR ALTIVEC */
#if defined(__APPLE__) /* For OSX */
#define ALTI_SL1 (vector unsigned int)(SL1, SL1, SL1, SL1)
#define ALTI_SR1 (vector unsigned int)(SR1, SR1, SR1, SR1)
#define ALTI_MSK (vector unsigned int)(MSK1, MSK2, MSK3, MSK4)
#define ALTI_MSK64 \
(vector unsigned int)(MSK2, MSK1, MSK4, MSK3)
#define ALTI_SL2_PERM \
(vector unsigned char)(1,2,3,23,5,6,7,0,9,10,11,4,13,14,15,8)
#define ALTI_SL2_PERM64 \
(vector unsigned char)(1,2,3,4,5,6,7,31,9,10,11,12,13,14,15,0)
#define ALTI_SR2_PERM \
(vector unsigned char)(7,0,1,2,11,4,5,6,15,8,9,10,17,12,13,14)
#define ALTI_SR2_PERM64 \
(vector unsigned char)(15,0,1,2,3,4,5,6,17,8,9,10,11,12,13,14)
#else /* For OTHER OSs(Linux?) */
#define ALTI_SL1 {SL1, SL1, SL1, SL1}
#define ALTI_SR1 {SR1, SR1, SR1, SR1}
#define ALTI_MSK {MSK1, MSK2, MSK3, MSK4}
#define ALTI_MSK64 {MSK2, MSK1, MSK4, MSK3}
#define ALTI_SL2_PERM {1,2,3,23,5,6,7,0,9,10,11,4,13,14,15,8}
#define ALTI_SL2_PERM64 {1,2,3,4,5,6,7,31,9,10,11,12,13,14,15,0}
#define ALTI_SR2_PERM {7,0,1,2,11,4,5,6,15,8,9,10,17,12,13,14}
#define ALTI_SR2_PERM64 {15,0,1,2,3,4,5,6,17,8,9,10,11,12,13,14}
#endif /* For OSX */

其他 SFMT-paramsXXXXXX 头文件有:SFMT-params607.hSFMT-params1279.hSFMT-params2281.hSFMT-params4253.hSFMT-params11213.hSFMT-params44497.hSFMT-params86243.hSFMT-params216091.h

我更改并修改了所有这些参数文件。换句话说,我清理了头文件中所有不必要的 OS X 特定代码。

下面,您可以看到 SFMT-params19937.h 文件中定义的其他必要参数

#define POS1 122 // the pick up position of the array.
#define SL1 18 // the parameter of shift left as four 32-bit registers.
#define SL2 1 // the parameter of shift left as one 128-bit register.
#define SR1 11 // the parameter of shift right as four 32-bit registers.
#define SR2 1 // the parameter of shift right as one 128-bit register.

/* A bitmask, used in the recursion. These parameters are introduced
to break symmetry of SIMD. */
#define MSK1 0xdfffffefU
#define MSK2 0xddfecb7fU

#define MSK3 0xbffaffffU
#define MSK4 0xbffffff6U

// These definitions are part of a 128-bit period certification vector.
#define PARITY1 0x00000001U
#define PARITY2 0x00000000U
#define PARITY3 0x00000000U
#define PARITY4 0x13c9e684U

// String representation of MEXP 19937 parameters.
#define IDSTR "SFMT-19937:122-18-1-11-1:dfffffef-ddfecb7f-bffaffff-bffffff6"

SFMT.h 文件修改

SFMT.h 头文件非常重要。我将把这个文件添加到我的项目中。当然,它是一个头(*.H)文件,所以我把它添加到我项目的 Header Files 目录中。在对其进行一些修改后,我将能够从 DLL 外部调用 SFMT 函数。在讨论这些更改之前,让我们看看 SFMT.h 函数、声明及其任务

  1. uint32_t gen_rand32(void):此函数的任务是生成伪随机 32 位整数。此方法称为顺序调用方法。
  2. uint64_t gen_rand64(void):此函数的任务是生成伪随机 64 位整数。此方法称为顺序调用方法。
  3. int fill_array32(uint32_t *array, int size):此函数可以用伪随机 32 位整数填充数组。函数的第一个参数是用于填充伪随机 32 位整数的数组。函数的第二个参数是此数组的大小。此外,第二个参数表示生成的 32 位整数的数量。此方法称为块调用方法。如果函数失败,返回值为 0。
  4. int fill_array64(uint64_t *array, int size):此函数可以用伪随机 64 位整数填充数组。函数的第一个参数是用于填充伪随机 64 位整数的数组。函数的第二个参数是此数组的大小。此外,第二个参数表示生成的 64 位整数的数量。此方法称为块调用方法。如果函数失败,返回值为 0。
  5. void init_gen_rand(uint32_t seed):此函数使用 32 位整数种子初始化内部状态数组。参数 seed 是用作种子的 32 位整数。

要从我的 DLL 外部调用这些 SFMT 函数,我需要使用一个特殊关键字

__declspec(dllexport):您可以使用 __declspec(dllexport) 关键字从 DLL 导出数据、函数、类或类成员函数。__declspec(dllexport) 将导出指令添加到对象文件,因此您不需要使用 .def 文件。许多导出指令,例如序数、NONAMEPRIVATE,只能在 .def 文件中进行,并且无法在没有 .def 文件的情况下指定这些属性。但是,除了使用 .def 文件之外,使用 __declspec(dllexport) 不会导致生成错误。

要导出 SFMT 函数,如果指定了调用约定关键字,则 __declspec(dllexport) 关键字必须出现在该关键字的左侧。例如:

__declspec(dllexport) int fill_array32(uint32_t *array, int size):

__declspec(dllexport) 将函数名存储在 DLL 的导出表中。

为了使代码更具可读性,我将在 SFMT 头文件的开头为 __declspec(dllexport) 定义一个宏,并将在我们要导出的每个函数中使用此宏

#define DllExport __declspec( dllexport )

经过这些修改,我们的 SFMT 函数变成了可导出形式。您可以在下面看到它们。

DllExport uint32_t gen_rand32(void);
DllExport uint64_t gen_rand64(void);
DllExport int fill_array32(uint32_t *array, int size);
DllExport int fill_array64(uint64_t *array, int size);
DllExport void init_gen_rand(uint32_t seed);

函数的实数版本:在 SFMT.h 文件中,您可以看到一些函数的实数版本。它们是由 Isaku Wada 提供的,用于生成随机实数。所有实数函数都是内联函数。内联函数不能作为 DLL 的一部分编译。内联函数意味着它被编译到调用它的位置。这意味着内联函数没有地址,因为函数在每次调用它的地方都会被复制(例如,在主应用程序中)。如果您想将其作为单独的二进制库(*.lib, *.dll 等),导出的函数就不能是内联的——实际上,它们位于二进制文件中,而不是在您的可执行代码中。由于这些原因,我清理了 SFMT.h 文件中的内联函数,然后将 rSFMT.cpp 文件添加到我项目中的 Source Files 目录。此文件包含函数的实数版本,但不是内联版本。然后,我将它们设置为可导出形式,如下所示:

//Exporting rSFMT.cpp functions:
DllExport double to_real1(uint32_t v);
DllExport double genrand_real1(void);
DllExport double to_real2(uint32_t v);
DllExport double genrand_real2(void);
DllExport double to_real3(uint32_t v);
DllExport double genrand_real3(void);
DllExport double to_res53(uint64_t v);
DllExport double to_res53_mix(uint32_t x, uint32_t y);
DllExport double genrand_res53(void) ;
DllExport double genrand_res53_mix(void);

extern C:进行这些修改后,如果您编译 SFMT DLL 并调用导出的函数,则会在运行时收到类似如下的错误消息:

Sample Image

此问题发生是因为 C++ 编译器会修饰函数名以实现函数重载。让我们使用强大的 Windows 实用程序 dumpbin.exe 查看我们函数的确切名称。我们的命令是 dumpbin -exports SFMT.dll。此命令提示符的结果如下所示:

Sample Image

如您在此命令提示符中所见,函数名称不清晰,当我们尝试调用它们时,总是会发生未处理的异常。

没有修饰函数名称的标准方法。因此,您必须告诉 C++ 编译器不要修饰函数名称。我们将使用 extern C 结构来不修饰我们的函数。

SFMT.h 文件的开头

#ifdef __cplusplus
  extern "C" {
#endif

SFMT.h 文件的末尾

#ifdef __cplusplus
  }
#endif

现在,我们在此 extern C 结构之间编写的代码和函数将正常工作并易于调用。此时,让我们看看 dumpbin -exports SFMT.dll 命令的结果

Sample Image

SFMT-sse2.h 文件

如果您查看 SFMT.c 文件,您会看到以下代码:

#if defined(HAVE_SSE2)
  #include "SFMT-sse2.h"
#endif

此代码意味着,如果我们在项目命令行中包含 HAVE_SSE2 定义,则项目将使用 SFMT-sse2.h 文件。因此,如果您检查 SFMT-sse2.h 文件,您会发现此文件是为了利用 CPU SSE2 特殊指令的强大功能而编写的。当然,使用此文件会使我们的代码更快。使用此文件的第一个也是唯一一个限制是它只能在支持 SSE2 的 CPU 上运行。

使用 SSE2 支持以及如何启用此功能将在下一节“设置项目属性”中提及。

设置项目属性和优化

在 Visual Studio 中,在“项目”菜单下,单击“SFMT 属性…”。

将显示一个名为“SFMT 属性页”的新窗口。在此窗口左侧,“配置属性”选项卡下,您可以看到我们将使用的一些属性类别(常规、调试、C/C++ 等)。

Sample Image

首先,在项目属性窗口的上部,点击“配置管理器”按钮,配置管理器将显示在屏幕上。在此窗口中,将“配置”参数设置为 Release。同时,将“活动解决方案配置”也设置为 Release。将此参数设置为 Release 意味着编译我们的项目不需要调试数据,并且已准备好发布。

Sample Image

我们 SFMT.dll 项目最重要的属性是预处理器

在“配置属性”-->“C/C++”-->“预处理器”选项卡下,有预处理器定义。我将在此处添加两个定义:MEXPHAVE_SSE2MEXP 之前已经提到过,它代表梅森指数。此外,HAVE_SSE2 定义用于利用 CPU 的 SSE2 支持。

Sample Image

我想说的是,在这种情况下,更改 MEXP 值或消除 SSE2 支持非常灵活。您可以随时配置这两个预处理器定义,然后轻松编译另一个版本的 SFMT.dll

Sample Image

另一个重要的属性是“优化”。在“配置属性”-->“C/C++”-->“优化”下,请确保将“优化”设置为“最大化速度 (/O2)”。将此属性设置为“最大化速度 (/O2)”意味着编译器在编译项目时将生成一些优化输出。这可能会增加 SFMT.dll 的大小,但也可以忽略不计。因为,SFMT.dll 的速度优先于更大的大小。当我们生成两三个随机数时,拥有更快的代码并非必要,但是当生成 1000 万个数字时,代码的速度成为一个主要因素。在数学运算或工程应用等时间关键型应用程序中,也许,更快的代码可能更合适。

此外,我们还必须了解另一个选项,称为“启用内部函数 (/Oi)”。使用内部函数的程序速度更快,因为它们没有函数调用的开销,但由于创建了额外的代码,可能会更大。

Sample Image

在“配置属性”-->“C/C++”-->“代码生成”选项卡中,运行时库选项的默认值为 多线程 DLL (/MD)。我将把此选项更改为 多线程 (/MT)。这将使您的应用程序使用运行时库的多线程静态版本。它定义 _MT,并导致编译器将库名称 LIBCMT.lib 放入 .obj 文件中,以便链接器将使用 LIBCMT.lib 来解析外部符号。

Windows 上的 C/C++ 多线程应用程序需要使用 -MT-MD 选项进行编译。-MT 选项将使用静态库 LIBCMT.LIB 进行链接,而 -MD 将使用动态库 MSVCRT.LIB 进行链接。与 -MD 链接的二进制文件会更小但依赖于 MSVCRT.DLL,而与 -MT 链接的二进制文件会更大但会独立于运行时。实际的工作代码包含在 MSVCR90.DLL(适用于 Visual Studio 2008 项目)中,该 DLL 在运行时必须可用于与 MSVCRT.lib 链接的应用程序。

如果我使用 –MD 选项(动态链接)构建我的项目,那么我的 SFMT.dll 将大约为 10 KB。它相当小。如果我使用 –MT 选项(静态链接)构建项目,那么我的 SFMT.dll 将为 57 KB。当然,它比 10 KB 大。

另一方面,如果我尝试在其他计算机上调用和使用动态链接的 SFMT.dll,可能会收到这样的错误:

“此应用程序未能启动,因为应用程序配置不正确。重新安装应用程序可能会解决此问题。”

此错误表明您尝试在其上运行 SFMT.dll 的计算机和操作系统没有 C/C++ 运行时库。在这种情况下,您必须随 SFMT.dll 一起分发 C/C++ 运行时库。您可以在下面看到 SFMT.dll 在没有 C/C++ 运行时库的操作系统上运行的分析。如您所见,它需要 MSVCR90.dll 和相关的库。另请注意,使用必要的 C/C++ 运行时库设置 SFMT 项目非常简单。因为我们正在使用一个强大的 IDE:Visual Studio 2008。

Sample Image

此外,在“配置属性”-->“C/C++”-->“代码生成”选项卡中,将“启用增强指令集”属性设置为 流式 SIMD 扩展 2 (/arch:SSE2)arch 标志允许使用支持增强指令集(例如,Intel 32 位处理器的 SSE 和 SSE2 扩展)的处理器上的指令。请注意,此设置将阻止代码在不支持 SSE2 扩展的处理器上运行。但是,在此项目中,我们的处理器目标是支持 SSE 指令的 CPU。

在 C/C++ 选项卡下设置这些属性后,我们的命令行是

/O2 /Oi /GL /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_USRDLL"
/D "SFMT_EXPORTS" /D "MEXP=19937" /D "HAVE_SSE2" /D "_WINDLL" /D
"_UNICODE" /D "UNICODE" /FD /EHsc /MT /Gy /arch:SSE2
/Fo"Release\\" /Fd"Release\vc90.pdb" /W3 /nologo /c /Zi /TP /errorReport:prompt

Sample Image

在另一个名为“链接器”的选项卡上,将“目标机器”属性设置为 MachineX86 很重要。这是我们项目的默认值,但不要忘记检查它。链接器选项卡的命令将是这样的

/OUT:"C:\Users\emre\Documents\Visual Studio 2008\Projects\SFMT\Release\SFMT.dll"
/INCREMENTAL:NO /NOLOGO /DLL /MANIFEST
/MANIFESTFILE:"Release\SFMT.dll.intermediate.manifest"
/MANIFESTUAC:"level='asInvoker' uiAccess='false'" /DEBUG
/PDB:"C:\Users\emre\Documents\Visual Studio 2008\Projects\SFMT\Release\SFMT.pdb"
/SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /LTCG /DYNAMICBASE /NXCOMPAT
/MACHINE:X86 /ERRORREPORT:PROMPT kernel32.lib user32.lib gdi32.lib
winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib
uuid.lib odbc32.lib odbccp32.lib

构建项目并分析 SFMT.dll

现在,是时候构建 SFMT 项目了。要做到这一点,只需按 F6 键,或将焦点放在 Visual Studio 的“构建”菜单上,然后单击“构建解决方案”。如果一切正常,您将收到一条“构建成功”消息。之后,Visual Studio 将在 SFMT 项目主目录下创建一个名为“Release”的文件夹。在此文件夹中,您将看到 SFMT.dll。为了分析 SFMT.dll,我使用了 Dependency Walker 工具。您可以从这里下载它。通过此 GUI,可以轻松查看 SFMT.dll 中所有可导出的函数。您可以在下面看到一张显示 SFMT.dll 的屏幕截图。

此外,在构建我的项目后,我将 SFMT.dll 重命名为 SFMTsse2.dll,以备将来兼容。实际上,在确定和使用正确的 DLL 时,我将需要这种类型的标准。无论如何,我们稍后会讨论它。

Sample Image

没有 SSE2 支持

如果 SFMT.dll 将运行的机器上没有 SSE2 支持,那么您会收到错误。为了避免这个错误,您可以轻松地准备 C 版本的 SFMT.dll 并将其重命名为 SFMTc.dll。这个 SFMTc.dll 可以在不需要 SSE2 支持的情况下生成随机数。为 SFMTc.dll 配置项目属性非常容易:

  1. 在“配置属性”-->“C/C++”-->“预处理器”选项卡下,有预处理器定义。从此窗口中删除“HAVE_SSE2”预处理器命令。
  2. 在“配置属性”-->“C/C++”-->“代码生成”选项卡中,将“启用增强指令集”属性设置为 未设置
  3. 重新生成您的项目,然后将项目 release 目录中的 SFMT.dll 重命名为 SFMTc.dll

就是这样。您可以在不支持 SSE2 的机器上使用您的 SFMTc.dll

新文章

“SFMT 实战”系列的新文章即将推出。

再会。

参考文献

历史

  • 2008年12月02日:首次发布
  • 2009年4月26日:发布1.1版本。在此版本中:
    • 添加了一些必要的辅助函数
    • 填充方法已改进,以便灵活使用
    • release 目录中添加了 SFMTc.dll。此 DLL 不需要 SSE2 支持。
    • 现在,两个 DLL(SFMTsse2.dllSFMTc.dll)都包含版本信息。
© . All rights reserved.