GMP库使用简介

在Go,C#,Java,Erlang,Python等语言中都内置有大数的计算,笔者曾经写过一篇博文专门对比测试它们的性能:使用斐波那契(Fibonacci)数列来测试各大语言的性能

在C/C++中虽然标准库并没有内置大数的计算,但是有可选择的高性能的第三方库,比较有名的是GMP库OpenSSL的大数计算。其中GMP库号称是全球最快的大数计算库,没有之一。

本文就简单介绍一下如何使用它,然后再使用它写一个斐波那契(Fibonacci)数列来看看性能,可以对比一下前述的几大语言的测试。

一、安装

在MinGW下使用命令安装:

pacman -S mingw-w64-x86_64-gmp

Ubuntu下使用命令安装:

sudo apt install libgmp-dev

MSVC使用,可以去官方下载源码自行编译。

二、常用函数简介

gmp库的类型分为整数mpz_t,有理数mpq_t,浮点数mpf_t,自然数mp_limb_t。整数类的函数以mpz_开头,有理数以mpq_开头,浮点数以mpf_开头,还有一些非常底层的操作自然数的函数以mpn_开头,它们快速,高效,但是难以使用。

由于GMP库是C语言API,所以需要遵循C语言的使用规范。所有的数据在使用前都需要初始化(分配内存)再使用,使用完后还必须释放资源(释放内存)。

下面以整数为例介绍一些常用的函数,其它的可以类推。

1. 初始化

void mpz_init (mpz_t x)
初始化x为0。

void mpz_inits (mpz_t x, …)
如果有多个变量需要初始化,可以使用此函数一次性初始化完,要求以0结尾,比如mpz_inits (a, b, c, 0)。

2. 释放

void mpz_clear (mpz_t x)
释放x使用的内存

void mpz_clears (mpz_t x, …)
同初始化一样,如果有多个变量需要释放内存,可以使用此函数一次性释放完,同样要求以0结尾,mpz_clears (a, b, c, 0)。

3. 赋值

void mpz_set (mpz_t rop, const mpz_t op)
void mpz_set_ui (mpz_t rop, unsigned long int op)
void mpz_set_si (mpz_t rop, signed long int op)
void mpz_set_d (mpz_t rop, double op)
void mpz_set_q (mpz_t rop, const mpq_t op)
void mpz_set_f (mpz_t rop, const mpf_t op)
这些函数都是将op赋值给rop。其中 mpz_set_d, mpz_set_q 和 mpz_set_f 会截断 op为一个整数.

int mpz_set_str (mpz_t rop, const char *str, int base)
使用字符串str中的数据来赋值给rop,base代表基数,可以是2到62,如果是0,则使用字符串str中的数据前导来决定,0x/0X表示十六进制,0b/0B表示二进制,0表示八进制,其它表示十进制。对于基数不超过36的情况,字母大小写不做区分;大写字母与小写字母的取值相同。对于基数在37至62之间的情况,大写字母对应常规的10…35,小写字母对应36…61。
若整个字符串是指定进制下的有效数字,该函数返回0;否则返回-1。

void mpz_swap (mpz_t rop1, mpz_t rop2)
交换两个数的值。

4. 初始与赋值的组合

void mpz_init_set (mpz_t rop, const mpz_t op)
void mpz_init_set_ui (mpz_t rop, unsigned long int op)
void mpz_init_set_si (mpz_t rop, signed long int op)
void mpz_init_set_d (mpz_t rop, double op)
int mpz_init_set_str (mpz_t rop, const char *str, int base)
在定义了mpz_t整数后,如果想初始化与赋值一起,可以使用上面这些函数。但是需要注意的是不能对已经初始化了的mpz_t整数调用上面这些函数,这样会导致内存泄漏。

5. 转换

unsigned long int mpz_get_ui (const mpz_t op)
signed long int mpz_get_si (const mpz_t op)
double mpz_get_d (const mpz_t op)
char * mpz_get_str (char *str, int base, const mpz_t op)
前面的赋值是将C语言中的标准类型转换为GMP中的mpz_t类型,如果想要转换回来就使用上面这些函数。

double mpz_get_d_2exp (signed long int *exp, const mpz_t op)
将操作数转换为双精度浮点数,必要时进行截断(即向零舍入),并单独返回指数。
返回值的范围为 0.5<=abs(d)<1,指数会存储至 *exp。d * 2^exp 即为经截断处理的操作数数值。若操作数为零,则返回值为 0.0,且 *exp 中存储的值为 0。
这与标准 C 语言的 frexp 函数类似。

6. 算术运算

void mpz_add (mpz_t rop, const mpz_t op1, const mpz_t op2)
void mpz_add_ui (mpz_t rop, const mpz_t op1, unsigned long int op2)
加法运算: r o p = o p 1 + o p 2 rop = op1 + op2 rop=op1+op2

void mpz_sub (mpz_t rop, const mpz_t op1, const mpz_t op2)
void mpz_sub_ui (mpz_t rop, const mpz_t op1, unsigned long int op2)
void mpz_ui_sub (mpz_t rop, unsigned long int op1, const mpz_t op2)
减法运算: r o p = o p 1 − o p 2 rop = op1 - op2 rop=op1op2

Function: void mpz_mul (mpz_t rop, const mpz_t op1, const mpz_t op2)
Function: void mpz_mul_si (mpz_t rop, const mpz_t op1, long int op2)
Function: void mpz_mul_ui (mpz_t rop, const mpz_t op1, unsigned long int op2)
乘法运算: r o p = o p 1 ∗ o p 2 rop = op1 * op2 rop=op1op2

Function: void mpz_addmul (mpz_t rop, const mpz_t op1, const mpz_t op2)
Function: void mpz_addmul_ui (mpz_t rop, const mpz_t op1, unsigned long int op2)
加乘积运算: r o p + = o p 1 ∗ o p 2 rop += op1 * op2 rop+=op1op2

Function: void mpz_submul (mpz_t rop, const mpz_t op1, const mpz_t op2)
Function: void mpz_submul_ui (mpz_t rop, const mpz_t op1, unsigned long int op2)
减乘积运算: r o p − = o p 1 ∗ o p 2 rop -= op1 * op2 rop=op1op2

Function: void mpz_mul_2exp (mpz_t rop, const mpz_t op1, mp_bitcnt_t op2)
运算: r o p = o p 1 ∗ 2 o p 2 rop = op1 * 2^{op2} rop=op12op2,该操作也可定义为按op2位进行左移。

Function: void mpz_neg (mpz_t rop, const mpz_t op)
负数运算: r o p = − o p rop = -op rop=op

Function: void mpz_abs (mpz_t rop, const mpz_t op)
取绝对值运算

void mpz_cdiv_q (mpz_t q, const mpz_t n, const mpz_t d)
void mpz_cdiv_r (mpz_t r, const mpz_t n, const mpz_t d)
void mpz_cdiv_qr (mpz_t q, mpz_t r, const mpz_t n, const mpz_t d)
unsigned long int mpz_cdiv_q_ui (mpz_t q, const mpz_t n, unsigned long int d)
unsigned long int mpz_cdiv_r_ui (mpz_t r, const mpz_t n, unsigned long int d)
Function: unsigned long int mpz_cdiv_qr_ui (mpz_t q, mpz_t r, const mpz_t n, unsigned long int d)
unsigned long int mpz_cdiv_ui (const mpz_t n, unsigned long int d)
void mpz_cdiv_q_2exp (mpz_t q, const mpz_t n, mp_bitcnt_t b)
void mpz_cdiv_r_2exp (mpz_t r, const mpz_t n, mp_bitcnt_t b)
void mpz_fdiv_q (mpz_t q, const mpz_t n, const mpz_t d)
void mpz_fdiv_r (mpz_t r, const mpz_t n, const mpz_t d)
void mpz_fdiv_qr (mpz_t q, mpz_t r, const mpz_t n, const mpz_t d)
unsigned long int mpz_fdiv_q_ui (mpz_t q, const mpz_t n, unsigned long int d)
unsigned long int mpz_fdiv_r_ui (mpz_t r, const mpz_t n, unsigned long int d)
unsigned long int mpz_fdiv_qr_ui (mpz_t q, mpz_t r, const mpz_t n, unsigned long int d)
unsigned long int mpz_fdiv_ui (const mpz_t n, unsigned long int d)
void mpz_fdiv_q_2exp (mpz_t q, const mpz_t n, mp_bitcnt_t b)
void mpz_fdiv_r_2exp (mpz_t r, const mpz_t n, mp_bitcnt_t b)
void mpz_tdiv_q (mpz_t q, const mpz_t n, const mpz_t d)
void mpz_tdiv_r (mpz_t r, const mpz_t n, const mpz_t d)
void mpz_tdiv_qr (mpz_t q, mpz_t r, const mpz_t n, const mpz_t d)
unsigned long int mpz_tdiv_q_ui (mpz_t q, const mpz_t n, unsigned long int d)
unsigned long int mpz_tdiv_r_ui (mpz_t r, const mpz_t n, unsigned long int d)
unsigned long int mpz_tdiv_qr_ui (mpz_t q, mpz_t r, const mpz_t n, unsigned long int d)
unsigned long int mpz_tdiv_ui (const mpz_t n, unsigned long int d)
void mpz_tdiv_q_2exp (mpz_t q, const mpz_t n, mp_bitcnt_t b)
void mpz_tdiv_r_2exp (mpz_t r, const mpz_t n, mp_bitcnt_t b)
除法运算
将 n 除以 d,得到商 q 和余数 r。对于 2exp 函数, d = 2 b d=2^b d=2b。舍入共有三种模式,分别适用于不同场景:

  • cdiv 函数会将 q 向正无穷方向向上取整,且 r 的符号与 d 相反。其中字母 c 是“ceil(向上取整)”的缩写。
  • fdiv 会将商 q 向负无穷方向向下取整,余数 r 的符号将与除数 d 一致。其中的 f 代表“floor(向下取整)”。
  • tdiv 会将 q 向零方向取整,且 r 的符号将与 n 一致。其中 t 代表“截断(truncate)”。

在所有情况下,q 和 r 都满足 n = q ∗ d + r n = q*d + r n=qd+r,且 r 满足 0 ≤ ∣ r ∣ < ∣ d ∣ 0 ≤ |r| < |d| 0r<d

7.取模运算

void mpz_mod (mpz_t r, const mpz_t n, const mpz_t d)
unsigned long int mpz_mod_ui (mpz_t r, const mpz_t n, unsigned long int d)
将 r 设为 n 对 d 取模的结果。该运算会忽略除数的符号,所得结果始终为非负值。

8. 指数运算

void mpz_pow_ui (mpz_t rop, const mpz_t base, unsigned long int exp)
void mpz_ui_pow_ui (mpz_t rop, unsigned long int base, unsigned long int exp)
将rop设置为base的exp次幂,即: r o p = b a s e e x p rop=base^{exp} rop=baseexp。其中0的0次方的结果为1。

void mpz_powm (mpz_t rop, const mpz_t base, const mpz_t exp, const mpz_t mod)
void mpz_powm_ui (mpz_t rop, const mpz_t base, unsigned long int exp, const mpz_t mod)
将rop设置为(base的exp次方)对mod取模的结果。

9. 求根运算

void mpz_sqrt (mpz_t rop, const mpz_t op)
将 rop 设置为操作数平方根的截断整数部分,即: r o p = o p rop=\sqrt{op} rop=op

int mpz_root (mpz_t rop, const mpz_t op, unsigned long int n)
void mpz_rootrem (mpz_t root, mpz_t rem, const mpz_t u, unsigned long int n)
void mpz_sqrtrem (mpz_t rop1, mpz_t rop2, const mpz_t op)
int mpz_perfect_power_p (const mpz_t op)
int mpz_perfect_square_p (const mpz_t op)

10. 比较运算

int mpz_cmp (const mpz_t op1, const mpz_t op2)
int mpz_cmp_d (const mpz_t op1, double op2)
int mpz_cmp_si (const mpz_t op1, signed long int op2)
int mpz_cmp_ui (const mpz_t op1, unsigned long int op2)
比较 op1 与 op2。若 op1 > op2 则返回正值,若 op1 = op2 则返回零,若 op1 < op2 则返回负值。
mpz_cmp_ui 和 mpz_cmp_si 均为宏,会对其参数进行多次求值。调用 mpz_cmp_d 时可传入无穷大值,但传入 NaN 时结果未定义。

int mpz_cmpabs (const mpz_t op1, const mpz_t op2)
int mpz_cmpabs_d (const mpz_t op1, double op2)
int mpz_cmpabs_ui (const mpz_t op1, unsigned long int op2)
比较操作数op1与op2的绝对值:若abs(op1) > abs(op2)则返回正值,若abs(op1) = abs(op2)则返回零,若abs(op1) < abs(op2)则返回负值。
mpz_cmpabs_d 可传入无穷大参数调用,但传入 NaN 时结果未定义。

int mpz_sgn (const mpz_t op)
若 op > 0 则返回 +1,若 op = 0 则返回 0,若 op < 0 则返回 -1。
该函数实际是以宏的形式实现的,会对其参数进行多次求值。

11. 逻辑与位运算

void mpz_and (mpz_t rop, const mpz_t op1, const mpz_t op2)
位与运算:rop = op1 & op2

void mpz_ior (mpz_t rop, const mpz_t op1, const mpz_t op2)
位或运算:rop = op1 | op2

void mpz_xor (mpz_t rop, const mpz_t op1, const mpz_t op2)
位异或运算:rop = op1 ^ op2

void mpz_setbit (mpz_t rop, mp_bitcnt_t bit_index)
置位运算,将rop的第bit_index位设置为1

void mpz_clrbit (mpz_t rop, mp_bitcnt_t bit_index)
清位运算,将rop的第bit_index位设置为0

int mpz_tstbit (const mpz_t op, mp_bitcnt_t bit_index)
测试rop的第bit_index位,根据值返回1或者0

12. 格式化输出

A、格式化输出字符

GMP库的格式化输出函数与标准C的格式化输出非常相似。只是添加了自己的格式化输出字符:
GMP 分别为 mpz_t、mpq_t 和 mpf_t 新增了类型“Z”、“Q”和“F”,为 mp_limb_t 新增了类型“M”,为 mp_limb_t 数组新增了类型“N”。“Z”、“Q”、“M”和“N”的表现与整数类似。若有需要,“Q”会打印出“/”及分母。“F”的表现与浮点数类似。

B、格式化输出函数

int gmp_printf (const char *fmt, …)
int gmp_vprintf (const char *fmt, va_list ap)
输出至标准输出流 stdout。返回写入的字符数,若发生错误则返回 -1。

int gmp_fprintf (FILE *fp, const char *fmt, …)
int gmp_vfprintf (FILE *fp, const char *fmt, va_list ap)
输出至流 fp。返回写入的字符数,若出错则返回 -1。

int gmp_sprintf (char *buf, const char *fmt, …)
int gmp_vsprintf (char *buf, const char *fmt, va_list ap)
在缓冲区(buf)中构造一个以空字符结尾的字符串,返回写入的字符数,不包含结尾的空字符。
buf 指向的存储空间与 fmt 字符串之间不允许存在重叠区域。
不建议使用这些函数,因为没有任何防护机制可防止缓冲区 buf 的可用空间被超出。

int gmp_snprintf (char *buf, size_t size, const char *fmt, …)
int gmp_vsnprintf (char *buf, size_t size, const char *fmt, va_list ap)
在 buf 中构造一个以空字符结尾的字符串。写入的字节数不会超过 size。若要获得完整输出,size 的值必须足够容纳该字符串及其结尾的空字符。
返回值为应生成的字符总个数,不包含结尾的空字符。若返回值 retval 大于等于 size,则实际输出会被截断为前 size-1 个字符,并在末尾追加一个空字符。
区域 {buf,size} 与 fmt 字符串之间不允许存在重叠。
请注意,返回值采用的是 ISO C99 snprintf 风格,即便 C 库中的 vsnprintf 是较早的 GLIBC 2.0.x 风格,情况也是如此。

三、计算斐波那契(Fibonacci)数列

C源码,main.c:

#include <gmp.h>

int main(int argc, char* argv[]) {
  mpz_t a;
  mpz_t b;
  mpz_t sum;
  mpz_init(sum);
  mpz_init_set_si(a, 1);
  mpz_init_set_si(b, 1);
  for (int i = 0; i < 1000000; ++i)
  {
    mpz_add(sum, a, b);
    mpz_set(b, a);
    mpz_set(a, sum);
  }
  gmp_printf("%Zd\n", sum);
  mpz_clears(a, b, sum, 0);
  return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)

project(t)

find_package(PkgConfig)
pkg_check_modules(pkgLibs REQUIRED IMPORTED_TARGET gmp)

aux_source_directory(. SRC)

add_executable(${PROJECT_NAME} ${SRC})
target_include_directories(${PROJECT_NAME} PRIVATE ${pkgLibs_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::pkgLibs)

或者直接使用命令编译:

gcc -O2 -o t main.c -lgmp

在MinGW下使用:time ./t运行结果(多次运行,以最少的一次为准):
在这里插入图片描述
测试了一下Debug以及Release版本,发现基本没差别。结果有点出乎意料啊,不得不佩服Go的性能!

为了公平,这里的C源码并没有使用GMP计算斐波那契数的专用函数:mpz_fib_ui/mpz_fib2_ui,因为其它语言没有这样的专用函数,使用了就不公平。

虽说Go语言在循环中使用了Set:

b.Set(a)
a.Set(sum)

C语言也使用了set:

mpz_set(b, a);
mpz_set(a, sum);

但Go语言应该对这种情况的内存分配做了极致优化,而mpz_set是内存的深度复制,会严重影响性能,而它有另一个API:mpz_swap,可以有效避免深度复制,做到重复利用。
改一下测试代码:

#include <gmp.h>

int main(int argc, char* argv[]) {
  mpz_t a;
  mpz_t b;
  mpz_t c;
  mpz_init(c);
  mpz_init_set_si(a, 1);
  mpz_init_set_si(b, 1);
  for (int i = 0; i < 1000000; ++i) {
    mpz_add(c, a, b);
    mpz_swap(b, a);
    mpz_swap(a, c);
  }
  gmp_printf("%Zd\n", a); // 最后结果在a变量中
  mpz_clears(a, b, c, 0);
  return 0;
}

测试结果:

在这里插入图片描述
这回终于符合预期了,只用了1.932秒,比Go快三分之一左右。

那如果使用GMP库计算斐波那契数的专用函数进行测试呢:

#include <gmp.h>

int main(int argc, char* argv[]) {
  mpz_t a;
  mpz_t b;
  mpz_init(a);
  mpz_init(b);
  mpz_fib2_ui(a, b, 1000002); // 由于原来的计算是预先设置了两项:1,1,所以这里需要加2
  gmp_printf("%Zd\n", a);
  mpz_clears(a, b, 0);
  return 0;
}

结果也是出乎意料,这回是出乎意料的快:只需要0.095秒,果然GMP是大数计算的性能之王。
在这里插入图片描述

各测试对比表:

语言Code Runner结果Code Runner 执行的命令命令行编译运行结果命令行编译运行的命令命令行直接运行结果命令行直接运行的命令
Go3.548秒go run main.go3.539秒time go run main.go3.068秒time ./main
C#10.295秒dotnet run10.075秒time dotnet run9.255秒time ./ConsoleApp
Java10.654秒javac t.java && java t//9.814秒time java t
Python5.939秒set PYTHONIOENCODING=utf-8 && python.exe -u main.py5.943秒time python main.py//
Erlang13.353秒escript app.erl//12.836秒time erl -noshell -s app main 0 -s init stop
C语言迭代法使用mpz_set////4.29秒time ./t
C语言迭代法使用mpz_swap////1.932秒time ./t
C语言使用专用函数mpz_fib2_ui////0.095秒time ./t

总结:Go语言不愧是集快速开发与高性能平衡得最好的语言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值