这个表达式在C预处理器中是否正确

时间:2013-03-07 00:03:15

标签: c embedded c-preprocessor

当我发送变量x时,我想在 C 预处理器包含语句中执行以下算术功能。

#define calc_addr_data_reg (x) ( base_offset + ((x/7) * 0x20) + data_reg_offset)

如何使用bitshifts实现除法和乘法运算?在除法运算中,我只需要商。

1 个答案:

答案 0 :(得分:4)

回答问题,

  

" C预处理器中的表达式是否正确?"

我没有发现任何问题。

  

如何使用bitshifts实现除法和乘法运算?在除法运算中,我只需要商。

与几乎所有情况相比,编译器在优化代码方面做得更好。如果你不得不问StackOverflow如何做到这一点,那么你就不会知道足以胜过GCC。我知道我当然不会。但是因为你在这里问到gcc如何优化它。

@EdHeal,

这需要更多的空间来正确回应。你在给出的例子(getter和setter)中绝对正确,但在这个特定的例子中,inline函数会略微增加二进制的一边,假设它被称为一些次。

GCC将函数编译为:

mov ecx, edx
mov edx, -1840700269
mov eax, edi
imul    edx
lea eax, [rdx+rdi]
sar eax, 2
sar edi, 31
sub eax, edi
sal eax, 5
add esi, eax
lea eax, [rsi+rcx]
ret

哪个字节比用于调用和从函数获取返回值的程序集更多,这是3 push语句,调用,返回和pop语句(可能)。

用-Os编译成:

mov eax, edi
mov ecx, 7
mov edi, edx
cdq
idiv    ecx
sal eax, 5
add eax, esi
add eax, edi
ret

哪个字节少于调用返回push并弹出。

因此,在这种情况下,无论代码在内联时是否更小或更大,他使用的编译器标志都非常重要。

再次给Op:

解释那里的代码含义:

这篇文章的下一部分直接来自:http://porn.quiteajolt.com/2008/04/30/the-voodoo-of-gcc-part-i/


对这种怪异的正确反应是“等等。”我认为一些具体的指示可以使用更多的解释:

movl $-1840700269, -4(%ebp)
以八进制表示的

-1840700269 = -015555555555(由前导零表示)。我将使用八进制表示,因为它看起来更酷。

imull %ecx

这会使%ecx和%eax相乘。这两个寄存器都包含一个32位数字,因此这种乘法可能会产生一个64位数字。这不能适合一个32位寄存器,因此结果分为两部分:产品的高32位放入%edx,低32放入%eax。

leal (%edx,%ecx), %eax

这会添加%edx和%ecx,并将结果放入%eax。 lea的表面目的是用于地址计算,将它写成两个指令会更清楚:add和mov,但这需要两个时钟周期来执行,而这只需要一个。

另请注意,此指令使用前一条指令的高32位乘法(存储在%edx中),然后覆盖%eax中的低32位,因此只使用乘法中的高位。 / p>

sarl $2, %edx   # %edx = %edx >> 2

从技术上讲,sar(算术右移)是否等同于>>运算符是实现定义的。 gcc保证运算符是带符号数的算术移位(“带符号'>>'通过符号扩展对负数作用”),并且因为我已经使用过gcc一次,所以我假设我正在使用它这篇文章的其余部分(因为我)。

sarl $31, %eax

%eax是一个32位寄存器,因此它将在[-231,231-1]范围内的整数上运行。这产生了一些有趣的东西:这个计算只有两个可能的结果。如果数字大于或等于0,则无论如何,班次都会将数字减少到0。如果数字小于0,则结果为-1。

这是一个非常直接的将这个程序集重写回C的过程,为了安全起见,有一些整数宽度的偏执,因为其中一些步骤依赖于整数正好是32位宽:

int32_t divideBySeven(int32_t num) {
    int32_t eax, ecx, edx, temp; // push %ebp / movl %esp, %ebp / subl $4, %esp
    ecx = num; // movl 8(%ebp), %ecx
    temp = -015555555555; // movl $-1840700269, -4(%ebp)
    eax = temp; // movl -4(%ebp), %eax

    // imull %ecx - int64_t casts to avoid overflow
    edx = ((int64_t)ecx * eax) >> 32; // high 32 bits
    eax = (int64_t)ecx * eax; // low 32 bits

    eax = edx + ecx; // leal (%edx,%ecx), %eax
    edx = eax; // movl %eax, %edx
    edx = edx >> 2; // sarl $2, %edx

    eax = ecx; // movl %ecx, %eax
    eax = eax >> 31; // sarl $31, %eax

    ecx = edx; // movl %edx, %ecx
    ecx = ecx - eax; // subl %eax, %ecx
    eax = ecx; // movl %ecx, %eax
    return eax; // leave / ret
}

现在显然有很多低效的东西:不必要的局部变量,一堆不必要的变量交换,以及eax =(int64_t)ecx * eax1;根本不需要(我只是为了完成而把它包括在内)。所以让我们清理一下。下一个清单只消除了大部分残骸,每个区块上方都有相应的组件:

int32_t divideBySeven(int32_t num) {
    // pushl %ebp
    // movl %esp, %ebp
    // subl $4, %esp
    // movl 8(%ebp), %ecx
    // movl $-1840700269, -4(%ebp)
    // movl -4(%ebp), %eax
    int32_t eax, edx;
    eax = -015555555555;

    // imull %ecx
    edx = ((int64_t)num * eax) >> 32;

    // leal (%edx,%ecx), %eax
    // movl %eax, %edx
    // sarl $2, %edx
    edx = edx + num;
    edx = edx >> 2;

    // movl %ecx, %eax
    // sarl $31, %eax
    eax = num >> 31;

    // movl %edx, %ecx
    // subl %eax, %ecx
    // movl %ecx, %eax
    // leave
    // ret
    eax = edx - eax;
    return eax;
}

最终版本:

int32_t divideBySeven(int32_t num) {
    int32_t temp = ((int64_t)num * -015555555555) >> 32;
    temp = (temp + num) >> 2;
    return (temp - (num >> 31));
}

我还没有回答这个显而易见的问题,“为什么他们会这样做?”答案当然是速度。在第一个列表中使用的整数除法指令idiv需要高达43个时钟周期才能执行。但是gcc产生的无分割方法有更多的指令,所以整体上它真的更快吗?这就是我们有基准的原因。

int main(int argc, char *argv[]) {
    int i = INT_MIN;
    do {
        divideBySeven(i);
        i++;
    } while (i != INT_MIN);

    return 0;
}

循环每个可能的整数?当然!我为这两种实现运行了五次测试,并随着时间的推移计时。 gcc的用户CPU时间分别为45.9,4.89,45.9,45.99和46.11秒,而使用idiv指令进行汇编的时间分别为62.34,62.32,62.44,62.3和62.29秒,这意味着天真实现的运行时间约为36%平均较慢。耀。

编译器优化是一件很美好的事情。


好的,我回来了,为什么这有效呢?

int32_t divideBySeven(int32_t num) {
    int32_t temp = ((int64_t)num * -015555555555) >> 32;
    temp = (temp + num) >> 2;
    return (temp - (num >> 31));
}

让我们来看看第一部分:

int32_t temp = ((int64_t)num * -015555555555) >> 32;

为什么这个号码?

好吧,让我们取2 ^ 64并将它除以7,然后看看弹出的是什么。

2^64 / 7 = 2635249153387078802.28571428571428571429

这看起来像一团糟,如果我们把它转换成八进制怎么办?

0222222222222222222222.22222222222222222222222

这是一个非常漂亮的重复模式,肯定不是巧合。我的意思是我们记得7是0b111并且我们知道当我们除以99时,我们倾向于在基数10中得到重复模式。因此,当我们在基数8中得到重复模式时,我们知道这是有意义的。除以7。

那么我们的号码在哪里?

(int32_t)-1840700269(uint_32t)2454267027

相同

* 7 = 17179869189

最后17179869184是2^34

这意味着17179869189是7 2 ^ 34的最接近倍数。换句话说, 2454267027是适合uint32_t的最大数字,当乘以7非常接近2的幂

这个八进制数是多少?

0222222222223

为什么这很重要?好吧,我们想要除以7.这个数字是2 ^ 34/7 ...大约。因此,如果我们乘以它,然后leftshift 34次,我们应该得到一个非常接近确切数字的数字。

最后两行看起来像是用来修补近似误差。

也许在这个领域拥有更多知识和/或专业知识的人可以参与其中。

>>> magic = 2454267027
>>> def div7(a):
...   if (int(magic * a >> 34) != a // 7):
...     return 0
...   return 1
... 
>>> for a in xrange(2**31, 2**32):
...   if (not div7(a)):
...     print "%s fails" % a
... 

失败从3435973841开始,这很有趣0b11001100110011001100110011010001