如何在C中优化这段代码

时间:2012-03-19 09:28:04

标签: c

昨天,在一次访谈中,我被要求测试一个数字中的第5位(以测试其是否打开和关闭)以及下面是我如何完成它。

int number = 16;
int mask   = 1<<5;

if ((number & mask) == 0)
    printf("Bit is off");
else
    printf("its on");

但他随后要求我优化此代码并在不使用此特定掩码的情况下执行此操作。

所以我的问题是我在这段代码中可以使用的其他掩码?

11 个答案:

答案 0 :(得分:6)

也许面试官想看看你对一个简单挑战的反应。或者只是想知道你是否真的了解C,并坚持自己的立场?也许面试官想知道你是否知道非零是真的,因此测试你对C的理解深度?或者也许你是否可以在头脑中做二进制到十六进制?

恕我直言的采访比代码更多很多。它们做起来很昂贵。我总是试图给人一个清晰的印象,通过书面沟通,甚至通过电话很难做到。毕竟,其中一些人将与新兵一起工作!

最紧凑,可能最快的可能是:

int number = 16;  // is this really what they gave?

printf((number & 0x20)?"its on":"Bit is off"); // did they mean 5th or bit 5?

编辑:

我编写了原始方法和我的替代方案,并为ARM Coretx-M3 / 4编译了它(这是我目前正在编写的处理器)。它是用-O3编译的。然后我反汇编每个编译文件(使用objdump)来获取汇编程序。我是这样做的,因为gcc -S的输出很大;这包括汇编器和链接器的大量额外信息,这使得查找代码变得更加困难。

我用dummy_printf替换了printf以避免#including stdio.h,这增加了更多的噪音。 dummy_printf与printf不完全相同,只需要一个参数,但它会使输出保持简短: - )

源(附加的所有7个文件使其更易于阅读)位于: http://pastebin.com/PTeApu9n

结果的objdump连接输出(对于每个.o)位于: http://pastebin.com/kHAmakE3

如您所见,原文编译为:

void original_bit5(int number) {
    int mask = 1<<5;

    if ((number & mask) == 0)
   0:   f010 0f20   tst.w   r0, #32
   4:   d005        beq.n   1a <original_bit5+0x1a>
        dummy_printf("Bit is off");
    else
        dummy_printf("its on"); 
   6:   f240 0000   movw    r0, #0
   a:   f2c0 0000   movt    r0, #0
   e:   f7ff bffe   b.w 0 <dummy_printf>

void original_bit5(int number) {
    int mask = 1<<5;

    if ((number & mask) == 0)
        dummy_printf("Bit is off");
  12:   f240 0000   movw    r0, #0
  16:   f2c0 0000   movt    r0, #0
  1a:   f7ff bffe   b.w 0 <dummy_printf>
  1e:   bf00        nop

我认为对dummy_printf的调用是使用尾调用链接,即dummy_printf不会返回此函数。效率很高!

没有函数入口代码,因为前四个函数参数在寄存器r0-r3中传递。

您无法在r0中看到正在加载的两个字符串的地址。那是因为没有联系。

你可以看到:

int mask = 1<<5;    
if ((number & mask) == 0)

编译为:

   0:   f010 0f20   tst.w   r0, #32
   4:   d005        beq.n   1a <original_bit5+0x1a>

所以1<<5(... == 0)是编译器的更直接和有效的指令序列。有适当调用dummy_printf的分支。

我的代码编译为:

void my_bit5(int number) {
    dummy_printf((number & 0x20)?"its on":"Bit is off");    
   0:   f240 0200   movw    r2, #0
   4:   f240 0300   movw    r3, #0
   8:   f010 0f20   tst.w   r0, #32
   c:   f2c0 0200   movt    r2, #0
  10:   f2c0 0300   movt    r3, #0
  14:   bf14        ite ne
  16:   4610        movne   r0, r2
  18:   4618        moveq   r0, r3
  1a:   f7ff bffe   b.w 0 <dummy_printf>
  1e:   bf00        nop

这似乎也得到尾调用优化,即没有返回此函数,因为不需要一个,dummy_printf的返回将直接返回main()

你看不到的是两个寄存器,r2和r2将包含两个字符串的地址。那是因为没有联系。

正如您所看到的,有一个条件执行指令'ite',它将寄存器r2或寄存器r3加载到参数寄存器r0中。所以这段代码中没有分支。

对于带流水线的简单处理器,这可能非常有效。在简单的流水线处理器上,分支可以导致“管道”停顿,同时清除管道的某些部分。这因处理器而异。所以我假设gcc已经做对了,并且生成了比执行分支更好的代码序列。我没有检查过。

在伦丁的激励下,我想出了这个:

void union_bit5(int number) {
    union { int n; struct { unsigned :5; unsigned bit :1; }; } tester;
    tester.n = number;

    if (tester.bit)
        dummy_printf("Bit is on");
    else
        dummy_printf("its off");    
}

它没有明确包含掩码或位移。它几乎肯定是编译器依赖的,你必须测试它以确保它的工作(glerk! - (

ARM的gcc生成相同的代码(bne vs beq,但可以调整)作为OP的解决方案,因此没有优化,但它会删除掩码:

void union_bit5(int number) {
    union { int n; struct { unsigned :5; unsigned bit :1; }; } tester;
    tester.n = number;

    if (tester.bit)
   0:   f010 0f20   tst.w   r0, #32
   4:   d105        bne.n   1a <union_bit5+0x1a>
        dummy_printf("Bit is on");
    else
        dummy_printf("its off");    
   6:   f240 0000   movw    r0, #0
   a:   f2c0 0000   movt    r0, #0
   e:   f7ff bffe   b.w 0 <dummy_printf>
void union_bit5(int number) {
    union { int n; struct { unsigned :5; unsigned bit :1; }; } tester;
    tester.n = number;

    if (tester.bit)
        dummy_printf("Bit is on");
  12:   f240 0000   movw    r0, #0
  16:   f2c0 0000   movt    r0, #0
  1a:   f7ff bffe   b.w 0 <dummy_printf>
  1e:   bf00        nop

为了它的价值:

(number & 0x20)? dummy_printf("its on") : dummy_printf("Bit is off");

ARM的gcc生成与OP完全相同的代码。它生成分支,而不是条件指令。

摘要:

  1. 原始代码被编译为非常有效的指令序列
  2. 三元...?...:...运算符可以编译为不涉及ARM Cortex-M3 / 4上的分支的代码,但也可以生成正常的分支指令。
  3. 在这种情况下,编写比原始代码更高效的代码很困难: - )
  4. 我要补充一下,恕我直言,与比特测试相比,做一个printf的成本是如此巨大,担心优化比特测试的问题太小了;它失败了Amdahl's Law。比特测试的适当策略是确保使用-O3或-Os。


    如果你想做一些有点不正常的事情(特别是对于这样一个微不足道的问题),但可能会让采访者想到的不同,为每个字节值创建一个查找表。 (我不指望它会更快......)

    #define BIT5(val) (((val)&0x20)?1:0)
    const unsigned char bit5[256] = {
    BIT5(0x00),BIT5(0x01), BIT5(0x02),BIT5(0x03), 
    BIT5(0x04),BIT5(0x05), BIT5(0x06),BIT5(0x07),
    // ... you get the idea ...
    BIT5(0xF8),BIT5(0xF9), BIT5(0xFA),BIT5(0xFB), 
    BIT5(0xFC),BIT5(0xFD), BIT5(0xFE),BIT5(0xFF)
    };
    
    //...
    if (bit5[(unsigned char)number]) {
        printf("its on");
    } else {
        printf("Bit is off");
    }
    

    如果在外围寄存器中存在某些复杂的位模式(需要转换为决策或开关),这是一种方便的技术。它也是O(1)

    你可以将两者结合起来! - )

答案 1 :(得分:2)

检查位有两种方法:

if (number & (1 << bit)) { ... }
if ((number >> bit) & 1) { ... }

我认为这对你很有意思:http://graphics.stanford.edu/~seander/bithacks.html

答案 2 :(得分:1)

另一种方式是

1:将数字右移5次,使第5位从右边变为第0位(即LSB) 2:现在逻辑是LSB为1的数字为奇数,0为0的数字为偶数,请检查使用%2

如果您认为mod操作比位操作昂贵得多,我认为这完全取决于编译器。看看这个帖子

AND faster than integer modulo operation?

我不确定面试官为什么要求你进行优化,可能是他期待模数法作为答案。

答案 3 :(得分:1)

你确定要将它移动 5位吗? 怎么样:

int n = 16;
printf ("%d\n", (n >> 4) % 2); 

答案 4 :(得分:0)

您可以使用bit test assember instruction,但编译器不会接受您正在做的事情并且无论如何都要这样做。

除此之外,没有什么可以优化的,当然,唯一的方法是查看您的方法中可能的任何微小变化是否更快,这将是分析。

这是gcc 4.2.1 -O3为if((number >> 5) & 1))生成的代码:

0000000100000ee0    pushq   %rbp
0000000100000ee1    movq    %rsp,%rbp
0000000100000ee4    shrl    $0x05,%edi
0000000100000ee7    notl    %edi
0000000100000ee9    andl    $0x01,%edi
0000000100000eec    movl    %edi,%eax
0000000100000eee    leave
0000000100000eef    ret

if(number & (1 << 5))

0000000100000ee0    pushq   %rbp
0000000100000ee1    movq    %rsp,%rbp
0000000100000ee4    shrl    $0x05,%edi
0000000100000ee7    notl    %edi
0000000100000ee9    andl    $0x01,%edi
0000000100000eec    movl    %edi,%eax
0000000100000eee    leave
0000000100000eef    ret

所以我们看到至少gcc 4.2.1在这些情况下产生相同的代码,但它没有使用位测试指令。

答案 5 :(得分:0)

(number & 16)?printf("yes"):printf("no");

答案 6 :(得分:0)

c学习者新手的尝试

int number = 16;
if(16 == number&(0x10))
    puts("true");
else
    puts("false");

答案 7 :(得分:0)

每个人都向右转。我想成为原创并向左移动:

#define INDEX 5

int number = 16;

if (number<<(sizeof(number)*8-INDEX-1)<0)

  printf("Bit #%d is set in %d.\n", INDEX, number);
else    
  printf("Bit #%d is NOT set in %d.\n", INDEX, number);

这段代码很丑陋且绝对依赖于实现(C标准表示结果未定义)。在x86上它起作用并且它更有效,因为MSB总是复制在标志寄存器的#7位(&#34; sign&#34;)中,可以使用单个jns指令进行测试。

换句话说,对于INDEX 5,您有:

[...]
shl $0x1F, %eax
test %eax, %eax
jns 8053635
[...]

OP的原始解决方案更清晰,这就是生产代码应该如何。

答案 8 :(得分:0)

任何优化该代码的尝试都属于“过早优化”类别。如果您了解编译器如何将C转换为机器代码,则不会尝试优化该代码。我猜测面试官缺乏这样的知识。

如果我们剖析该代码,这就是我们得到的:

1<<5在编译时被翻译为文字32。写int mask = 1<<5;int mask = 32;之间的表现绝对没有区别,但后者难以理解。

此外,

  • if ((number & mask) == 0)完全等同于
  • if ((number & 32) == 0)完全等同于
  • if ((number & (1<<5)) == 0)

存在两种情况:

  • 编译器需要找到存储掩码的内存位置。
    • 如果用户声明了变量掩码,则该值将存储在那里。
    • 如果用户未声明变量,则该值将存储在不可见的临时变量中。
    • 上述两种情况下的RAM消耗完全相同。
  • 或者编译器不需要将掩码存储在任何地方。它将优化掉整个掩码变量或数字文字,并用程序指令的其余部分将它们加入。

将挑选这两个中的哪一个取决于是否从声明点到发生屏蔽的if语句修改int number = 16;

就是这样。任何以与原始示例不同的方式编写代码的尝试都是过早优化模糊处理,并且不会导致任何性能差异。

答案 9 :(得分:0)

原谅以下答案:

我曾经在初创公司工作,当公司决定不去追求候选人时,他们想出一个虚假的理由来结束面试。也许这就是海报的经历。

要求第k位可能意味着最低有效位是第0位,因此(数字&amp; 1&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt; 5但那不是问题。他要求优化。有时你面试失败的原因与你无关。在那种情况下,这是他们的损失;总会有另一个面试机会。

答案 10 :(得分:0)

在其中一次采访中,我给出了以下答案,他很满意,但问题的一点变化是'检查是否设置了第n位。

int N = 16;
printf ("%d\n", (N >> (n-1)) % 2); 

所以,当答案是通用的, 不完全确定这个例子中哪一个(下面的)更快。

1<<(n-1) & N (or)
N>>(n-1) % 2 (or)
N>>(n-1) & 1