【问题标题】:Bit setting and code readability位设置和代码可读性
【发布时间】:2009-07-28 22:27:30
【问题描述】:

我有一个 Arduino 应用程序(实际上是一个库),其中包含许多状态标志 - 最初我只是将它们声明为整数(在这种情况下是 uint8_t 所以 8 位无符号字符)。但我可以将它们全部组合成一个整数并使用位掩码操作来设置和测试状态。

前者的一个例子:

if (_shift == HIGH)
{
    _shift = LOW;
}
else
{
    _shift = HIGH;
}

后者的一个例子

#define SHIFT_BIT 0

if (bitRead(_flags, SHIFT_BIT) == HIGH)
{
   bitWrite(_flags, SHIFT_BIT, LOW);
}
else
{
   bitWrite(_flags, SHIFT_BIT, HIGH);
}

前者读起来更好,但后者效率更高(空间和时间)。在这种情况下,空间和时间效率应该总是获胜,还是这种优化只应在需要时进行?

(添加)

为了完整起见,这里是那些 bitWrite 等宏的接线定义:

#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))

【问题讨论】:

    标签: c++ embedded performance arduino


    【解决方案1】:

    在此问题上查看 Raymond Chen's excellent take。总之,您需要做一些详细的计算,以确定后一种情况是否实际上更有效,这取决于有多少对象与实际设置这些状态的调用点有多少。

    就可读性而言,看起来您正在使用成员变量执行此操作,这意味着您可能已经将它们封装在不错的函数中。在这种情况下,我并不关心可读性,因为至少使用该类的人的代码看起来不错。但是,如果有问题,您始终可以将其封装在私有函数中。

    【讨论】:

    • 在类实例很少的情况下,我同意其中的大部分内容。但是(a)他说的好像创建 1000 个结构实例是闻所未闻的,并且(b)自然词原子性的好处是值得怀疑的。多线程代码依赖于原子读写但从不需要原子更新的字段比具有 1000 个实例的结构要少得多。在大多数情况下,一旦您将其移至缺乏缓存一致性的架构,它就会中断。
    • 我会看看参考资料,谢谢。在这种情况下(这是一个管理按钮/LED组合的通用类)我无法想象实例化超过20个,但大约1000个实例是正确的,但在这种特殊情况下不是......
    • (稍后阅读)。这是一篇好文章。好消息是我可以通过查看目标应用程序的内存占用和相关的内存使用情况来直接衡量这一点。在我的目标芯片中,我只有 2k 的 ram 可用于“变量”,但 30k 可用于程序。所以通过增加后者来减少前者可能是一个很好的权衡......
    • 当您的系统具有 2k 内存和 16Mhz 处理器时,实例的实际数量会很快从 1000 个下降。
    • @Pete:很好。如果保存 16 字节的数据确实有价值,Chen 的分析根本不适用(或声称适用)。
    【解决方案2】:

    根据我不确定的 AVR-GCC 编译器的合规性,您可以做这样的事情并保持整洁。

    struct flags {
        unsigned int flag1 : 1;  //1 sets the length of the field in bits
        unsigned int flag2 : 4;
    }; 
    
    flags data;
    
    data.flag1 = 0;
    data.flag2 = 12;
    
    if (data.flag1 == 1)
    {
        data.flag1 = 0;
    }
    else
    {
        data.flag1 = 1;
    }
    

    如果您还想一次访问整个标志 int,那么:

    union 
    {
        struct {
            unsigned int flag1 : 1;  //1 sets the length of the field in bits
            unsigned int flag2 : 4;
        } bits;
        unsigned int val;
    } flags;
    

    然后您可以使用 2 级间接访问位:variable.bits.flag1variable.val

    【讨论】:

    • 开枪,是的,在我重新回到 C++ 的过程中,我奇怪地忘记了这种技术
    • 除上述内容外,您还可以为每个标志定义:#define f_EndOfFrameReached (data.endOfFrameReached),这会增加可读性。我从事过始终使用该方法的嵌入式项目,它既可维护又节省内存。程序员只需要了解约定及其陷阱(例如,当心非原子读取-修改写入,这意味着您可能不应该将这些与中断例程一起使用)。
    • 位域结构因引起麻烦而臭名昭著,因为编译器可以以意想不到的方式打包结构。仅当您 (1) 对编译器的行为持肯定态度,并且 (2) 永远不会更改编译器(例如只有一个编译器的专用处理器)时,才应使用此方法。
    • @msemack:这应该不是问题,只要您不尝试使用结构来“解码”字节流,无论是通过类型转换还是联合。这个问题不仅适用于位域打包,也适用于字节序和对齐问题。还是我误解了你的意思?
    【解决方案3】:

    如果您不需要使用常量HIGHLOW,通过拆分为两种方法会更清楚。只需制作bitSetbitClear 方法。 bitSet 将该位设置为HIGHbitClear 将该位设置为LOW。然后就变成了:

    #define SHIFT_BIT 0
    
    if (bitRead(_flags, SHIFT_BIT) == HIGH)
    {
        bitClear(_flags, SHIFT_BIT);
    }
    else
    {
        bitSet(_flags, SHIFT_BIT);
    }
    

    当然,如果您只有 HIGH == 1LOW == 0,则不需要 == 检查。

    【讨论】:

    • 在 Arduino/接线实现中,HIGH 等于 0x01,LOW 等于 0x00...
    【解决方案4】:

    在我看来,即使您的后一个代码仍然可读。通过为每个标志命名,可以毫不费力地阅读代码。

    一个糟糕的方法是使用“魔术”数字:

    if( _flags | 0x20 ) {  // What does this number mean?
       do_something();
    }
    

    【讨论】:

      【解决方案5】:

      如果你不需要优化,不要做,使用最简单的解决方案。

      如果你确实需要优化,你应该知道是为了什么:

      • 如果您只设置或清除该位而不是切换它,第一个版本会稍微快一些,因为这样您就不需要读取内存。

      • 第一个版本更好 w.r.t.并发。在第二个你有读-修改-写,所以你需要确保内存字节不是同时访问的。通常你会禁用中断,这会增加你的中断延迟。此外,忘记禁用中断可能会导致非常讨厌且难以找到的错误(我迄今为止遇到的最讨厌的错误就是这种类型)。

      • 第一个版本稍微好一点。代码大小(更少的闪存使用),因为每次访问都是一次加载或存储操作。第二种方法需要额外的位操作。

      • 第二个版本使用更少的 RAM,尤其是当你有很多这些位时。

      • 如果您想一次测试多个位(例如,设置的位之一),第二个版本也更快。

      【讨论】:

        【解决方案6】:

        如果您在谈论可读性、位集和 C++,为什么我在 std::bitset 上找不到任何内容?我知道嵌入式程序员竞赛对位掩码非常满意,并且由于其纯粹的丑陋(掩码,而不是种族:)而变得盲目,但是除了掩码和位域之外,标准库也有一个非常优雅的解决方案。

        一个例子:

        #include <bitset>
        
        enum tFlags { c_firstflag, c_secondflag, c_NumberOfFlags };
        
        ...
        
        std::bitset<c_NumberOfFlags> bits;
        
        bits.set( c_firstflag );
        if( bits.test( c_secondflag ) ) {
          bits.clear();
        }
        
        // even has a pretty print function!
        std::cout << bits << std::endl;// does a "100101" representation.
        

        【讨论】:

        • 不确定这个编译器是否支持 std 库(或者会占用太多内存而无法使用)。默认情况下没有标准输出。没有屏幕,如果启用它,可能只有一个串口接口......
        • @Alan Moore:cout 位只是为了炫耀它可以做什么。对测试非常有用!
        【解决方案7】:

        是不是说得太简单了:

        flags ^= bit;
        

        【讨论】:

          【解决方案8】:

          对于位域,最好使用逻辑运算,所以你可以这样做:

          if (flags & FLAG_SHIFT) {
             flags &= ~FLAG_SHIFT;
          } else {
             flags |= FLAG_SHIFT;
          }
          

          这现在具有前者的外观和后者的效率。现在你可以有宏而不是函数,所以(如果我做对了——它会是这样的):

          #define bitIsSet(flags,bit) flags | bit
          #define bitSet(flags,bit) flags |= bit
          #define bitClear(flags,bit) flags &= ~bit
          

          您没有调用函数的开销,代码再次变得更具可读性。

          我还没有玩过 Arduino,但我不知道可能已经有这类东西的宏。

          【讨论】:

          • 是的,很抱歉使用 Arduino/接线定义 - bitSet/bitWrite 等都是通过与您上面编写的完全相同或相似的宏完成的。挑战在于五个不同级别的宏的可读性,最终隐藏了你想要实现的真实目标......
          • 我认为位操作在 C 中是相当微不足道的,任何在嵌入式系统(实际上是 Arduino)上工作的人都会或应该熟悉这些结构。我认为您的第二段代码的问题可能只是宏的名称......与使用 Set、Clear 或 IsSet 相比,bitRead() 和 bitWrite() 似乎是奇怪的约定。但我们的理由不是:-)
          • 是的,有一个 bitWrite 允许定义是设置还是清除,then 还有 bitSet 和 bitClear 可以用少一个参数(即变量 + 位代替变量 + 位 + 0 或 1)。我希望(在查看头文件之前)这些差异会进行一些神奇的优化,但这只是某人的约定...... :-)
          • 一个潜在的问题是如果你有很多标志,因此有几个变量持有标志。那么'flags'和'bit'必须成对出现,程序员有责任不把它们弄混。 IE。是 bitSet(flags1, END_OF_FRAME_REACHED) 还是 bitSet(flags2, END_OF_FRAME_REACHED)?您可以通过命名约定来减轻它,例如bitSet(flags2, F2_END_OF_FRAME_REACHED)。
          • 在实现为宏时,请注意将 everything 括起来...另外,我认为您希望 bitIsSet() 宏/函数看起来更像:(((标志)&(位))!= 0)
          【解决方案9】:

          我想说的第一件事是: “#define SHIFT 0” 为什么不使用常量而不是宏呢?就效率而言,常量允许确定类型,从而确保之后不需要转换。

          至于你的技术效率: - 首先,去掉 else 子句(如果它的值已经是 HIGH ,为什么要把它设置为 HIGH ?) - 其次,更喜欢先有一些可读的东西,在内部使用位掩码的内联 setter / getter 可以完成这项工作,高效且可读。

          至于存储,对于 C++,我倾向于使用 bitset(结合枚举)。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-05-10
            • 1970-01-01
            • 2012-07-02
            • 1970-01-01
            • 2020-05-28
            • 1970-01-01
            • 1970-01-01
            • 2013-01-20
            相关资源
            最近更新 更多