大整数算法[10] Comba乘法(实现)
★ 引子
上一篇文章讲了 Comba 乘法的原理,这次来讲讲如何实现。为了方便移植和充分发挥不同平台下的性能,暂时用了三种不同的实现方式:
1、单双精度变量都有的情况。
2、只有单精度变量的情况。
3、可以使用内联汇编的情况。
前面已经介绍了 Comba 乘法的原理和实现思路,为了方便,再把它贴到这里:
计算 c = a * b,c0,c1,c2 为单精度变量。
1. 增加 c 到所需要的精度,并且令 c = 0,c->used = a->used + b->used。
2. 令 c2 = c1 = c0 = 0。
3. 对于 i 从 0 到 c->used - 1 循环,执行如下操作:
3.1 ty = BN_MIN(i, b->used - 1)
3.2 tx = i - ty
3.3 k = BN_MIN(a->used - tx, ty + 1)
3.4 三精度变量右移一个数位:(c2 || c1 || c0) = (0 || c2 || c1)
3.5 对于 j 从 0 到 k - 1 之间执行循环,计算:
(c2 || c1 || c0) = (c2 || c1 || c0) + a(tx + j) * b(ty - j)
3.6 c(i) = c0
4. 压缩多余位,返回 c。
上面所说的三种不同实现方式,主要体现在 3.5 中的循环。 Comba 乘法的实现代码如下:(暂时不考虑 x = x * y 这种输入和输出是同一个变量的情况)
static void bn_mul_comba(bignum *z, const bignum *x, const bignum *y)
{
bn_digit c0, c1, c2;
bn_digit *px, *py, *pz;
size_t nc, i, j, k, tx, ty; pz = z->dp;
nc = z->used; c0 = c1 = c2 = 0; for(i = 0; i < nc; i++)
{
ty = BN_MIN(i, y->used - 1);
tx = i - ty;
k = BN_MIN(x->used - tx, ty + 1); px = x->dp + tx;
py = y->dp + ty; c0 = c1;
c1 = c2;
c2 = 0U; //Comba 32
for(j = k; j >= 32; j -= 32)
{
COMBA_INIT
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_STOP
}
//Comba 16
for(; j >= 16; j -= 16)
{
COMBA_INIT
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_STOP
}
//Comba 8
for(; j >= 8; j -= 8)
{
COMBA_INIT
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_STOP
}
//Comba 4
for(; j >= 4; j -= 4)
{
COMBA_INIT
COMBA_MULADDC COMBA_MULADDC
COMBA_MULADDC COMBA_MULADDC
COMBA_STOP
}
//Comba 1
for(; j > 0; j--)
{
COMBA_INIT
COMBA_MULADDC
COMBA_STOP
} *pz++ = c0;
} //for i bn_clamp(z);
}
在 3.5 步中的内循环,乘法的计算按照 1,4,8,16,32 的步进展开,展开的过程需要比较大的空间,但是减少了循环控制的开销,所以可以节省大量时间。执行乘法的关键代码都定义在 COMBA_INIT,COMBA_MULADDC 和 COMBA_STOP 这三个宏内。COMBA_INIT 用于变量定义或者初始化,COMBA_MULADDC 用于计算单精度乘法和累加,COMBA_STOP 用于存储当前计算结果。另外,这里假设 z 的精度已经足够。
★ 单双精度变量都有的情况
这种情况下,因为 bn_digit 和 bn_udbl 同时有定义,所以是最容易实现的一种。三个宏的定义如下:
#define COMBA_INIT \
{ \
bn_udbl r; \ #define COMBA_MULADDC \
\
r = (bn_udbl)(*px++) * (*py--) + c0; \
c0 = (bn_digit)r; \
r = (bn_udbl)c1 + (r >> biL); \
c1 = (bn_digit)r; \
c2 += (bn_digit)(r >> biL); \ #define COMBA_STOP \
}
在乘法器中,定义双精度变量 r 存储单精度乘法结果。在计算中,先计算单精度乘法,然后在与三精度变量 c2||c1||c0 相加。因为 c0,c1,c2 都是单精度数,为了避免溢出,先把累加计算的结果放到 r 上,然后提取低半部分作为当前数位的值。这里暂时不需要存储计算结果,所以 COMBA_STOP 没有什么操作。
★ 只有单精度变量的情况
这种情况下,bn_udbl 没有定义,每个数被拆成高半部分和低半部分,要执行 4 次单精度乘法,所以计算会比较复杂。
#define COMBA_INIT \
{ \
bn_digit a0, a1, b0, b1; \
bn_digit t0, t1, r0, r1; \ #define COMBA_MULADDC \
\
a0 = (*px << biLH) >> biLH; \
b0 = (*py << biLH) >> biLH; \
a1 = *px++ >> biLH; \
b1 = *py-- >> biLH; \
r0 = a0 * b0; \
r1 = a1 * b1; \
t0 = a1 * b0; \
t1 = a0 * b1; \
r1 += (t0 >> biLH); \
r1 += (t1 >> biLH); \
t0 <<= biLH; \
t1 <<= biLH; \
r0 += t0; \
r1 += (r0 < t0); \
r0 += t1; \
r1 += (r0 < t1); \
c0 += r0; \
c1 += (c0 < r0); \
c1 += r1; \
c2 += (c1 < r1); \ #define COMBA_STOP \
}
a1,a0 分别存储 x 的高半部分和低半部分,b1,b0 分别存储 y 的高半部分和低半部分,这两个操作可以通过将 x 和 y 向左向右移动半个数位得到。biLH 前面提到过,这里在说一下:biLH 是每个数位的比特大小的一半。上面这个乘法计算比较复杂,所以我先讲讲原理。
设 f(x),g(x) 分别表示单精度数 a 和 b,要计算 c = a * b。
a 和 b 用 f(x) 和 g(x) 表示:f(x) = a1 * x + a0,g(x) = b1 * x + b0,其中这里的 x 就是 2 ^ (n / 2)。
那么 h(x) = f(x) * g(x) = a1 * b1 * (x ^ 2) + (a1 * b0 + a0 * b1) * x + a0 * b0。这里默认 a0,a1,b0,b1 都是半精度的变量,所以他们的乘积可以用一个单精度变量存储。
令 t0 = a1 * b0 = p * x + q,t1 = a0 * b1 = r * x + s,r0 = a0 * b0,r1 = a1 * b1,其中 p,q,r,s 是半精度数,那么 h(x) 可以改写成:
h(x) = a1 * b1 * (x ^ 2) + (p * x + q + r * x + s) * x + a0 * b0
= a1 * b1 * (x ^ 2) + ((p + r) * x + (q + s)) * x + a0 * b0
= a1 * b1 * (x ^ 2) + (p + r) * (x ^ 2) + (q + s) * x + a0 * b0
= (a1 * b1 + p + r) * (x ^ 2) + (q + s) * x + a0 * b0
= (r1 + p + r) * (x ^ 2) + (q + s) * x + r0
所以,要计算 (r1 + p + r) * (x ^ 2),需要提取 t0,t1 的高半部分和 r1 相加(因为 r1 和 p,r 都是同类项,所以直接计算 x^2 的系数即可)。计算剩余项 (q + s) * x 和 r0 的和,需要将 t0 和 t1 的低半部分提取出来,左移半个数位后和 r0 相加(r0 是单精度数,而 q 和 s 是半个精度的,乘以 x 要左移半个数位)。计算 (q + s) * x + r0 后结果可能会溢出,表明产生了进位,所以还要把进位要传递到 (r1 + p + r) * (x ^ 2),最终乘积 c 就可以用 r0,r1 组成的双精度变量 r1||r0 表示。最后将乘积 c 和三精度变量 c2||c1||c0 累加即可。
★ 使用内联汇编的情况
注:本小节涉及到 x86 的汇编,如果对汇编不了解的话,可以跳过,免得浪费心情。对于 C 的内联汇编,细节暂时不说了,参考下面两篇文章:
【GCC 和 VC 的内联汇编】 https://github.com/1184893257/simplelinux/blob/master/inlineasm.md
【Linux 中 x86 的内联汇编】 http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/
如果编译环境可以使用内联汇编,则使用汇编指令执行乘法会加快计算速度。由于我只接触过 x86 平台的汇编,所以暂时只考虑 x86 平台,ARM,PowerPC 或者 MIPS 之类的架构暂时不管。 在 x86 环境下,需要考虑到 GCC 和 VC 环境下的内联汇编,毕竟他们的语法差别很大。
A. VC x86 环境下的内联汇编:(和 GCC 的比起来要简单得多)
#define COMBA_INIT \
{ \
__asm mov esi, px \
__asm mov edi, py \ #define COMBA_MULADDC \
\
__asm lodsd \
__asm mov ebx, [edi] \
__asm mul ebx \
__asm add c0, eax \
__asm adc c1, edx \
__asm adc c2, 0 \
__asm sub edi, 4 \ #define COMBA_STOP \
\
__asm mov px, esi \
__asm mov py, edi \
}
1. 首先把指针 px 和 py 的地址用 MOV 指令分别送到源变址寄存器 ESI 和目的变址寄存器 EDI。
2. 执行单精度乘法和累加:
A. 执行 LODSD 指令,把 ESI 寄存器指向的数据段中某单元的内容(也就是 x 的某个数位的值)送到 EAX 寄存器中,并根据方向标志和数据类型修改 ESI 寄存器的内容,具体的操作是将 ESI 中 的地址增加 4,相当于 C 中的 px++。一般情况下,方向标志 DF = 0, ESI 中的地址增加。如果 DF = 1,要执行 CLD 指令把 DF 置为 0,不过一般情况下不会出现这种情况的。
B. 用 MOV 指令把 EDI 寄存器所指向的数据段中某单元的内容(也就是 y 的某个数位的值)送到 EBX 寄存器中。
C. 执行 MUL 指令执行单精度乘法:(EDX,EAX)= EAX * EBX,计算 EAX 和 EBX 的乘积,结果的高位放在 EDX 寄存器中,结果的低位放在 EAX 寄存器中。
D. 执行 ADD 加法指令,将乘积的低位累加到三精度变量的最低位 c0 上。
E. 执行 ADC 带进位的加法指令,将乘积的高位和上一步加法产生的进位累加到三精度变量的第二个数位 c1 上。
F. 执行 ADC 带进位的加法指令,将剩余的进位加到三精度变量的最高位 c2 上。
G. 执行 SUB 减法指令,将 EDI 中的地址往前挪 4 字节,相当于 C 中的 py--。
3. 存储计算结果:用 MOV 指令把 ESI 和 EDI 中的地址送回到指针 px 和 py 中,之所以要做这一步,是因为在循环中,循环控制可能会修改 CPU 寄存器的值,如果在计算结束后不存储 ESI 和 EDI 的地址值,那么地址可能就会因为寄存器被修改而丢失。
B. GCC x86 环境下的内联汇编:(比较复杂)
#define COMBA_INIT \
{ \
asm \
( \
"movl %5, %%esi \n\t" \
"movl %6, %%edi \n\t" \ #define COMBA_MULADDC \
\
"lodsl \n\t" \
"movl (%%edi), %%ebx \n\t" \
"mull %%ebx \n\t" \
"addl %%eax, %2 \n\t" \
"adcl %%edx, %3 \n\t" \
"adcl $0, %4 \n\t" \
"subl $4, %%edi \n\t" \ #define COMBA_STOP \
\
"movl %%esi, %0 \n\t" \
"movl %%edi, %1 \n\t" \
:"=m"(px),"=m"(py),"=m"(c0),"=m"(c1),"=m"(c2) \
:"m"(px),"m"(py) \
:"%eax","%ebx","%ecx","%edx","%esi","%edi" \
); \
}
这段 GCC 的内联汇编指令和上面的 VC 内联汇编指令操作是一样的,只是语法不同而已,具体的语法请自行参考上面的资料或者 Google 搜索 :)
C. GCC x64 环境下的内联汇编:(和上面的 GCC x86 类似) 【补充】
考虑到在 64 bit 的 GCC 编译器下,可以使用 x64 的内联汇编,所以又加了这一部分代码。GCC x64 环境下的内联汇编和 GCC x86 的比较类似,只是寄存器和指令有些不同。细节可以参考这里:http://www.dutor.net/index.php/2013/11/att-syntax-and-gcc-inlined-assembly/
#define COMBA_INIT \
{ \
asm \
( \
"movq %5, %%rsi \n\t" \
"movq %6, %%rdi \n\t" \ #define COMBA_MULADDC \
\
"lodsq \n\t" \
"movq (%%rdi), %%rbx \n\t" \
"mulq %%rbx \n\t" \
"addq %%rax, %2 \n\t" \
"adcq %%rdx, %3 \n\t" \
"adcq $0, %4 \n\t" \
"subq $8, %%rdi \n\t" \ #define COMBA_STOP \
\
"movq %%rsi, %0 \n\t" \
"movq %%rdi, %1 \n\t" \
:"=m"(px),"=m"(py),"=m"(c0),"=m"(c1),"=m"(c2) \
:"m"(px),"m"(py) \
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi" \
); \
}
要注意的是:在 GCC x64 的环境下,bn_digit 是 64 bit的,也就是 8 个字节,所以执行完乘法后,py 的地址应该往前挪动 8 个字节,因此 COMBA_MULADDC 中最后的减法指令是把地址减去 8,而不是减 4。
★ 总结
Comba 乘法大概就讲这么多吧。由于 Comba 乘法的时间复杂度仍然是 O(n^2),所以当输入的规模 n 越大时,所需的时间仍然会急剧增加。下一篇文章将讲讲如何使用分治的方式降低乘法的时间复杂度。
【回到本系列目录】
版权声明
原创博文,转载必须包含本声明,保持本文完整,并以超链接形式注明作者Starrybird和本文原始地址:http://www.cnblogs.com/starrybird/p/4441022.html
大整数算法[10] Comba乘法(实现)的更多相关文章
- 大整数算法[09] Comba乘法(原理)
★ 引子 原本打算一篇文章讲完,后来发现篇幅会很大,所以拆成两部分,先讲原理,再讲实现.实现的话相对复杂,要用到内联汇编,要考虑不同平台等等. 在大整数计算中,乘法是非常重要的,因为 ...
- 大整数算法[11] Karatsuba乘法
★ 引子 前面两篇介绍了 Comba 乘法,最后提到当输入的规模很大时,所需的计算时间会急剧增长,因为 Comba 乘法的时间复杂度仍然是 O(n^2).想要打破乘法中 O(n^2) ...
- [转]大整数算法[11] Karatsuba乘法
★ 引子 前面两篇介绍了 Comba 乘法,最后提到当输入的规模很大时,所需的计算时间会急剧增长,因为 Comba 乘法的时间复杂度仍然是 O(n^2).想要打破乘法中 O(n^2) ...
- 大整数乘法(Comba 乘法 (Comba Multiplication)原理)
Comba 乘法以(在密码学方面)不太出名的 Paul G. Comba 得名.上面的笔算乘法,虽然比较简单, 但是有个很大的问题:在 O(n^2) 的复杂度上进行计算和向上传递进位,看看前面的那个竖 ...
- N!的阶乘附带简单大整数类的输入输出(暂时没有深入的了解)
Given an integer N(0 ≤ N ≤ 10000), your task is to calculate N! 我的思路:就想着大整数类去了,才发现自己还不能很好的掌握,其实这是一个大 ...
- 【老鸟学算法】大整数乘法——算法思想及java实现
算法课有这么一节,专门介绍分治法的,上机实验课就是要代码实现大整数乘法.想当年比较混,没做出来,颇感遗憾,今天就把这债还了吧! 大整数乘法,就是乘法的两个乘数比较大,最后结果超过了整型甚至长整型的最大 ...
- 算法笔记_034:大整数乘法(Java)
目录 1 问题描述 2 解决方案 2.1 蛮力法 1 问题描述 计算两个大整数相乘的结果. 2 解决方案 2.1 蛮力法 package com.liuzhen.chapter5; import ...
- [大整数乘法] java代码实现
上一篇写的“[大整数乘法]分治算法的时间复杂度研究”,这一篇是基于上一篇思想的代码实现,以下是该文章的连接: http://www.cnblogs.com/McQueen1987/p/3348426. ...
- 从大整数乘法的实现到 Karatsuba 快速算法
Karatsuba 快速乘积算法是具有独特合并过程(combine/merge)的分治算法(Karatsuba 是俄罗斯人).此算法主要是对两个整数进行相乘,并不适用于低位数(如 int 的 32 位 ...
随机推荐
- 【转】HP(惠普)大中华区总裁孙振耀退休感言
一.关于工作与生活我有个有趣的观察,外企公司多的是25-35岁的白领,40岁以上的员工很少,二三十岁的外企员工是意气风发的,但外企公司40岁附近的经理人是很尴尬的.我见过的40岁附近的外企经理人大多在 ...
- CSU 1160 把十进制整数转换为十六进制,格式为0x开头,10~15由大写字母A~F表示
题目链接: http://acm.hust.edu.cn/vjudge/contest/view.action?cid=82974#problem/A 解题思路: 将16进制的表示方法列在一个数组里面 ...
- Codeforces Round #232 (Div. 1) A 解题报告
A. On Number of Decompositions into Multipliers 题目连接:http://codeforces.com/contest/396/problem/A 大意: ...
- SQL中Merge的用法
SQL中Merge的用法 Merge的用法 Merge可以完成以下功能: 1. 两个表之间数据的更新 2. 进行进销存更新库存 3. 进行表之间数据的复制 语法说明: 1. 在语句结束后一定要 ...
- 关于xcode7编译旧项目崩溃-[UIApplication _runWithMainScene:transitionContext:completion:]
崩溃原因 crash: Assertion failure in -[UIApplication _runWithMainScene:transitionContext:completion:], / ...
- Retina 屏移动设备 1px解决方案
做移动端H5页面开发时都会遇到这样的问题,用 CSS 定义 1px 的实线边框,在 window.devicePixelRatio=2 的屏幕上会显示成 2px,在 window.devicePix ...
- python_Opencv_使用Matplotlib模块
使用Matplotlib模块 Matplotib 是python 的一个绘图库,里头有各种各样的绘图方法. 之后会陆续了解.先学习怎样用Matplotib 显示图像. 可以放大图像,保存图像. 安装M ...
- Robotium API -- 等待执行的方法sleep、waitFor
测试中经常出现明明有控件或者文本,但是由于界面没有载入完成或者其他原因导致assert判断的结果失败.或者两次执行,一次成功,一次失败的情况.所以需要加入这些等待某些控件或者文本载入的方法,以加强程序 ...
- Android中解析网络请求的URL
近期正在做Android网络应用的开发,使用了android网络请求方面的知识.如今向大家介绍网络请求方面的知识.我们知道android中向server端发送一个请求,(这就是我们通常所说的POST请 ...
- 2015-09-28 Javascript
1.Javascript是什么? JavaScript是一种脚本语言,结构简单,使用方便,其代码可以直接放入HTML文档中,可以直接在支持JavaScript的浏览器中运行.JavaSript. Ja ...