当我发送变量x
时,我想在 C 预处理器包含语句中执行以下算术功能。
#define calc_addr_data_reg (x) ( base_offset + ((x/7) * 0x20) + data_reg_offset)
如何使用bitshifts实现除法和乘法运算?在除法运算中,我只需要商。
答案 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