我在ARM Cortex-M0上运行了对时间要求严格的计算。这是我能想到的最快的32x32位-> 64位乘法。

以下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指令隔开一样。

评论


\ $ \ 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



#2 楼

尝试1

为了扩展我在上面相当长的评论,我从github上的此库实现中获取了64x64乘法,并将其修改为32x32乘法。我不确定您如何计算周期,但是这可能等效于您的26个周期的实现,因为我计算了19条“ 1-cycle”指令,该指令与原始帖子中的相同。我不知道pushpop需要经过多少个周期。

@ 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