以下C几乎将1-1编译为汇编指令:
//consider each arg as two 16-bit parts.
//r0 is Aa is A<<16+a. r1 is Bb is B<<16+b
//then Aa * Bb = (A*B)<<32 + (a*B+A*b)<<16 + a*b
// result = Hh<<32 + KmM<<16 + Ll
uint64_t mul32x32(uint32_t r0, uint32_t r1)
register uint32_t r2= r1&0xFFFF; // b
register uint32_t r3= r1>>16; // B
register uint32_t r4= r0&0xFFFF; // a
r1 = r0 >> 16; // A
r0 = r4; // a
r4 *= r2; // Ll = a*b
r0 *= r3; // a*B
r3 *= r1; // Hh = B*A
r2 *= r1; // b*A
r1 = 0; // K
ADD_W_CARRY(r1,r0,r2); // 0K.mM = a*B + b*A
r1 <<= 16; // K0
r2 = r0 >> 16; // 0m
r0 <<=16; // M0
DBL_ADD_W_CARRY(r1,r0,r2,r4); // K0.M0 += 0m.Ll
r1 += r3; // Km += Hh
return (((uint64_t)r1)<<32)|r0;
}
它使用此行内联汇编来处理C不能表达进位标志的问题:
//R.r+=x // clears x to avoid consuming another register
#define ADD_W_CARRY(R,r,x) __asm {\
ADDS r,r,x;\
MOVS x,0x0;\
ADCS R,R,x;}
//pure C equiv is: {r+=x;R+=(r<x);x=0;}
//R.r+=X.x
#define DBL_ADD_W_CARRY(R,r,X,x) __asm{\
ADDS r,r,x;\
ADCS R,R,X;}
//Pure C equiv is: {r+=x;R+=X+(r<x); }
此编译为:
PUSH {r4} ;// 2
LSRS r3,r0,#16 ;// 1
UXTH r2,r1 ;// 1
UXTH r0,r0 ;// 1
MOV r4,r0 ;// 1
LSRS r1,r1,#16 ;// 1
MULS r4,r2,r4 ;// 1
MULS r0,r1,r0 ;// 1
MULS r1,r3,r1 ;// 1
MULS r2,r3,r2 ;// 1
ADDS r0,r0,r2 ;// 1
MOVS r3,#0 ;// 1
MOVS r2,#0 ;// 1
ADCS r3,r3,r2 ;// 1
LSLS r2,r3,#16 ;// 1
LSRS r3,r0,#16 ;// 1
LSLS r0,r0,#16 ;// 1
ADDS r0,r0,r4 ;// 1
ADCS r2,r2,r3 ;// 1
POP {r4} ;// 2
ADDS r1,r2,r1 ;// 1
BX lr ;// 3
;== 26 cycles
有可能做得更好吗?
#1 楼
首先,是的,CortexM0缺乏在硬件上执行32x32 = 64乘法的任何方法。 CortexM3和CortexM4具有umull
指令,可让您真正轻松地执行32x32 = 64。是的,由于您是用C编写的,因此一种可能的实现方式是
uint64_t mul32x32(uint32_t r0, uint32_t r1) { return r0*(uint64_t)r1; }
,但是我想您已经尝试过了(使用
-O3
以及可以打开的其他优化和内联选项),发现编译器没有内联乘法,而是将其保留为调用一些内部libc函数。谷歌快速搜索发现了这个关于同一主题的上一个StackOverflow问题,其中有人与GCC对CortexM0的64x64 = 64乘法实现链接在一起(此处)。 ,建议您可以用手在整个对象中不断传播“已知高位为零”,这会给您带来体面的感觉。我不知道那是不是真的。
您是否还对
uint64_t mul32x32(uint32_t r0, uint32_t r1)
{
uint16_t r0h = r0 >> 16, r0l = r0 & 0xFFFF;
uint16_t r1h = r1 >> 16, r1l = r1 & 0xFFFF;
uint64_t result = (r0h * r1h);
result <<= 16;
result += r0h*r1l;
result += r0l*r1h;
result <<= 16;
result += r0l*r1l;
return result;
}
或等效地
的“幼稚”方法进行了基准测试
uint64_t mul32x32(uint32_t r0, uint32_t r1)
{
uint16_t r0h = r0 >> 16, r0l = r0 & 0xFFFF;
uint16_t r1h = r1 >> 16, r1l = r1 & 0xFFFF;
return ((uint64_t)(r0h * r1h) << 32)
+ ((uint64_t)(r0h * r1l) << 16)
+ ((uint64_t)(r0l * r1h) << 16)
+ ((uint64_t)(r0l * r1l) << 0);
}
?只要没有一种算术运算都可以转换为库函数调用,这便具有可移植ANSI C的优点,并且易于被编译器内联。如果您关心速度,那么内联的敏感性应该是您的#1关注点。
由于您可以访问编译器,因此我们不知道(我猜是Green Hills,来自
__asm{ }
语法)对于内联程序集块?),如果发布了由上述三个C实现产生的程序集,则可能会得到更好的答案。最后,请注意,CortexM0的
MULS
指令需要1个周期或32个周期,具体取决于处理器。如果您使用的是那些32周期处理器中的一个,则连续执行四条MULS
指令可能是最糟糕的事情之一。如果MULS
只需要1个周期,那么您可能还可以;我认为没有必要像在一个需要花费多个周期的机器(软件流水线)上将这些MULS
指令隔开一样。#2 楼
尝试1为了扩展我在上面相当长的评论,我从github上的此库实现中获取了64x64乘法,并将其修改为32x32乘法。我不确定您如何计算周期,但是这可能等效于您的26个周期的实现,因为我计算了19条“ 1-cycle”指令,该指令与原始帖子中的相同。我不知道
push
和pop
需要经过多少个周期。@ long long mul32(long r1, long r0)
@
@ Multiply r1 and r0 and return the product in r1:r0
@
.thumb_func
.global mul32
mul32:
push {r4, lr}
mov r2, r1
lsrs r1, r0, #16
lsrs r4, r2, #16
muls r1, r4
lsrs r3, r0, #16
uxth r0, r0
uxth r2, r2
muls r3, r2
muls r4, r0
muls r0, r2
movs r2, #0
adds r3, r4
adcs r2, r2
lsls r2, #16
adds r1, r2
lsls r2, r3, #16
lsrs r3, #16
adds r0, r2
adcs r1, r3
pop {r4, pc}
尝试2
实际上,我只是将以上内容转换为,它应该比原始帖子要快(现在的说明比以前的版本少1条)。也许您可以对其进行测试,以确保我没有做错什么,因为我将代码转换为头脑中的代码:
@ long long mul32(long r1, long r0)
@
@ Multiply r1 and r0 and return the product in r1:r0
@
.thumb_func
.global mul32
mul32:
push {r4, lr}
uxth r2, r1
lsrs r3, r0, #16
lsrs r1, r1, #16
mov r4, r1
muls r1, r3
uxth r0, r0
muls r3, r2
muls r4, r0
muls r0, r2
movs r2, #0
adds r3, r4
adcs r2, r2
lsls r2, #16
adds r1, r2
lsls r2, r3, #16
lsrs r3, #16
adds r0, r2
adcs r1, r3
pop {r4, pc}
评论
\ $ \ begingroup \ $
谢谢。推入和弹出操作需要1 +计数(寄存器)周期,因此在这两种情况下为3。在尝试2中,您似乎在muls r1,r3之前缺少mov r4,r1,以防止使用未初始化的寄存器。加上这一点,它就是24个循环。它执行相同的一组操作,除了我避免了额外的mov r3,#0。另一个区别是返回到调用方方法:push {r4,lr} ... pop {r4,pc}是6个周期vspush {r4} ... pop {r4}; bx lr是7。
\ $ \ endgroup \ $
– AShelly
16-2-4在20:43
\ $ \ begingroup \ $
@AShelly你是对的。我修正了尝试2,以添加您发现的缺失的mov r4,r1。
\ $ \ endgroup \ $
– JS1
16年4月4日在21:15
#3 楼
在某些情况下,您的代码返回了错误的输出。...检查以下输入的尝试2:-1
和-1
#4 楼
鉴于M0内核的某些实现需要32个周期来执行乘法,因此使用最小数量的MULS非常重要。现在,低32位由1 MULS正确计算,因此严格只需要3 MULS。如果仅使用r0-r3,这是另外4个周期(堆栈取消堆栈),并且看到乘法将是底部子例程,则lr(r14)仅在您的代码打算使用它时才需要堆栈。如果您缺少寄存器并且可以防止中断,那么也可以使用存储sp(r13)。问题在于,MULS仅更新Z&N标志。我绊脚石以一种优雅的方式来检测溢出。能比我设计出更好解决方案的人吗?
评论
\ $ \ begingroup \ $
请问您仅使用3 MULS []即可概述如何产生正确的64位结果
\ $ \ endgroup \ $
–灰胡子
18年5月15日在5:45
\ $ \ begingroup \ $
youtube.com/watch?v=JCbZayFr9RE 1-如果您经常从表中读取(<256个条目),请放在ROM的底部,即$ 00000000起。这样做使我的14-16周期CLZ(计数前导零)为12-14周期。地址是即时的! 2个中断在M0 / M0 + / M1的CPU中交换寄存器文件,因此可以随意将LR并存储SP到RAM中。 SP具有强大的指令和寻址模式。所有代码均基于SP寻址。 3-询问ARM社区。他们都是伟大的。如果您知道为什么LSxx指令使用寄存器的位0-7而不是寄存器0-4的原因,ARM不知道,所以如果解决,请发布。
\ $ \ endgroup \ $
– Sean
18-10-15在14:15
评论
\ $ \ begingroup \ $
谢谢。编译器是Keil uVision的编译器。处理器有1个周期乘法。如果我让编译器执行uint64_t乘法,它将生成对库例程__ARM_common_ll_muluu的调用,这需要35个周期。我将很快测试其他一些想法。
\ $ \ endgroup \ $
– AShelly
2015年12月21日在4:22
\ $ \ begingroup \ $
GCC的实现看起来与35个循环例程相同。
\ $ \ endgroup \ $
– AShelly
2015年12月21日在4:27
\ $ \ begingroup \ $
@AShelly您看过这个版本吗?您可以从此版本中删除3条以上的指令,因为该指令是64x64的乘法,因此不需要对高位进行运算的前几条指令。
\ $ \ endgroup \ $
– JS1
2015年12月21日在6:18
\ $ \ begingroup \ $
“天真”版本可编译为39个周期,“等效”版本可编译为38个周期。有关详细信息,请参见修订版4。 @ JS1是28-也许可以根据建议进行裁剪。
\ $ \ endgroup \ $
– AShelly
16-2-4在4:31
\ $ \ begingroup \ $
@AShelly由于这是64x64的乘法,因此您可以假装r1和r3以零开始。这使您可以删除行48-50和行55。然后将行52和54上的两个r3更改为r1以使寄存器正确。这样可以保存4条指令。但是,这假设您的输入在r3:r2和r1:r0中。在32位乘法中,您的输入在r1和r0中。您将需要使用一条额外的指令将r1复制到r2,因为在计算过程中代码会掩盖r1,因为r1将是输出之一。因此,总的来说,它应该是28-4 + 1 = 25个周期。
\ $ \ endgroup \ $
– JS1
16-2-4在5:27