【问题标题】:practical applications of bitwise operations [closed]按位运算的实际应用
【发布时间】:2010-10-07 15:44:42
【问题描述】:
  1. 您将按位运算用于什么目的?
  2. 它们为什么这么方便?
  3. 有人可以推荐一个非常简单的教程吗?

【问题讨论】:

标签: c# bitwise-operators


【解决方案1】:

虽然每个人似乎都迷上了标志用例,但这并不是位运算符的唯一应用(尽管可能是最常见的)。此外,C# 是一种足够高级的语言,其他技术可能很少使用,但仍然值得了解它们。以下是我能想到的:


<<>> 运算符可以快速乘以 2 的幂。当然,.NET JIT 优化器可能会为您执行此操作(以及其他语言的任何体面的编译器),但如果您'真的每微秒都在烦恼,你可以写这个来确定。

这些运算符的另一个常见用途是将两个 16 位整数填充到一个 32 位整数中。喜欢:

int Result = (shortIntA << 16 ) | shortIntB;

这对于直接与 Win32 函数交互很常见,有时出于遗留原因使用此技巧。

当然,当您想迷惑没有经验的人时,这些运算符很有用,例如在回答家庭作业问题时。 :)

在任何实际代码中,尽管使用乘法会好得多,因为它具有更好的可读性,并且 JIT 将其优化为 shlshr 指令,因此没有性能损失。


^ 运算符 (XOR) 有很多奇怪的技巧。这实际上是一个非常强大的运算符,因为有以下属性:

  • A^B == B^A
  • A^B^A == B
  • 如果您知道A^B,那么您就不可能知道AB 是什么,但是如果您知道其中之一,则可以计算出另一个。
  • 运算符不会遭受乘法/除法/加法/减法等任何溢出。

我看到使用这个运算符的几个技巧:

在没有中间变量的情况下交换两个整数变量:

A = A^B // A is now XOR of A and B
B = A^B // B is now the original A
A = A^B // A is now the original B

双向链表,每个项目只有一个额外的变量。这在 C# 中用处不大,但对于每个字节都很重要的嵌入式系统的低级编程可能会派上用场。

这个想法是您跟踪第一项的指针;最后一项的指针;对于您跟踪pointer_to_previous ^ pointer_to_next 的每个项目。这样,您可以从任一端遍历列表,但开销仅为传统链表的一半。下面是遍历的C++代码:

ItemStruct *CurrentItem = FirstItem, *PreviousItem=NULL;
while (  CurrentItem != NULL )
{
    // Work with CurrentItem->Data

    ItemStruct *NextItem = CurrentItem->XorPointers ^ PreviousItem;
    PreviousItem = CurrentItem;
    CurrentItem = NextItem;
}

要从末尾遍历,您只需将第一行从FirstItem 更改为LastItem。这是另一个节省内存的地方。

我在 C# 中定期使用 ^ 运算符的另一个地方是当我必须为我的复合类型计算 HashCode 时。喜欢:

class Person
{
    string FirstName;
    string LastName;
    int Age;

    public int override GetHashCode()
    {
        return (FirstName == null ? 0 : FirstName.GetHashCode()) ^
            (LastName == null ? 0 : LastName.GetHashCode()) ^
            Age.GetHashCode();
    }
}

【讨论】:

  • +1,漂亮的异或交换。谢谢。
  • +1 表示 xor-list 方法,以前没见过。
  • 有趣的是这被标记为不具建设性:)
  • “当你想迷惑没有经验的人时”+1 钉牢了太多高级开发人员/架构师的 MO。我真的认为经验会带来谦虚的实用主义,但可惜没有。 Brogrammers 已经接管了,世界变得更糟了。
  • and this +1000 "在任何实际代码中,尽管使用乘法会更好,因为它具有更好的可读性,并且 JIT 将其优化为 shl 和 shr 指令,所以有没有性能损失。”为什么代码高尔夫在实际代码中如此盛行?
【解决方案2】:

我在我的应用程序中使用按位运算符来保证安全性。我将在 Enum 中存储不同的级别:

[Flags]
public enum SecurityLevel
{
    User = 1, // 0001
    SuperUser = 2, // 0010
    QuestionAdmin = 4, // 0100
    AnswerAdmin = 8 // 1000
}

然后为用户分配他们的级别:

// Set User Permissions to 1010
//
//   0010
// | 1000
//   ----
//   1010
User.Permissions = SecurityLevel.SuperUser | SecurityLevel.AnswerAdmin;

然后检查正在执行的操作中的权限:

// Check if the user has the required permission group
//
//   1010
// & 1000
//   ----
//   1000
if( (User.Permissions & SecurityLevel.AnswerAdmin) == SecurityLevel.AnswerAdmin )
{
    // Allowed
}

【讨论】:

  • @justin,非常感谢。你能解释一下这个 User.Permissions = SecurityLevel.SuperUser | SecurityLevel.AnswerAdmin;
  • 这些只是标记的枚举。
  • @Dave - 没错。但是,当您使用标记枚举时,您正在使用按位运算符对值进行检查。方便且相当简单。在我看来,正是 OP 要求的。
  • @Justin:虽然现在已经过时,但 Enum.HasFlag:msdn.microsoft.com/en-us/library/system.enum.hasflag.aspx
  • @Reed - 是的。作为一个例子它仍然很有用(因为我猜测 Enum.HasFlags 的实现在内部会这样做)。
【解决方案3】:

我不知道解决你认为的数独问题有多实用,但让我们假设它是。

假设您想编写一个数独求解器,甚至只是一个简单的程序,它会向您显示棋盘并让您自己解决难题,但要确保移动是合法的。

棋盘本身很可能由一个二维数组表示,例如:

uint [, ] theBoard = new uint[9, 9];

0 表示单元格仍然是空的,并且范围 [1u, 9u] 中的值是板上的实际值。

现在假设您想检查某个动作是否合法。显然,您可以通过几个循环来完成,但是位掩码可以让您更快地完成任务。在仅确保遵守规则的简单程序中,这无关紧要,但在求解器中却可以。

您可以维护位掩码数组,其中存储有关已插入每行、每列 a 和每个 3x3 框的数字的信息。

uint [] maskForNumbersSetInRow = new uint[9];

uint [] maskForNumbersSetInCol = new uint[9];

uint [, ] maskForNumbersSetInBox = new uint[3, 3];

从数字到位模式的映射,一个位对应于那个数字集,非常简单

1 -> 00000000 00000000 00000000 00000001
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000100
...
9 -> 00000000 00000000 00000001 00000000

在 C# 中,您可以通过这种方式计算位模式(valueuint):

uint bitpattern = 1u << (int)(value - 1u);

1u 上面的行中,对应于位模式00000000 00000000 00000000 00000001 左移了value - 1。如果,例如value == 5,你会得到

00000000 00000000 00000000 00010000

一开始,每一行、每一列和每一框的掩码是0。每次在板上放一些数字时,都会更新掩码,因此设置了新值对应的位。

假设您在第 3 行插入值 5(行和列从 0 开始编号)。第 3 行的掩码存储在 maskForNumbersSetInRow[3] 中。我们还假设在插入之前,第 3 行中已经有数字 {1, 2, 4, 7, 9}。掩码 maskForNumbersSetInRow[3] 中的位模式如下所示:

00000000 00000000 00000001 01001011
bits above correspond to:9  7  4 21

目标是设置与此掩码中的值 5 对应的位。您可以使用按位或运算符 (|) 来完成。首先,您创建一个对应于值 5 的位模式

uint bitpattern = 1u << 4; // 1u << (int)(value - 1u)

然后您使用operator | 设置掩码maskForNumbersSetInRow[3] 中的位

maskForNumbersSetInRow[3] = maskForNumbersSetInRow[3] | bitpattern;

或使用更短的形式

maskForNumbersSetInRow[3] |= bitpattern;

00000000 00000000 00000001 01001011
                 |
00000000 00000000 00000000 00010000
                 =
00000000 00000000 00000001 01011011

现在您的掩码表明此行(第 3 行)中有值 {1, 2, 4, 5, 7, 9}

如果要检查,如果某个值在行中,可以使用operator &amp; 检查掩码中是否设置了相应的位。如果应用于掩码的该运算符的结果和对应于该值的位模式非零,则该值已经在该行中。如果结果为 0,则该值不在该行中。

例如,如果你想检查值 3 是否在行中,你可以这样做:

uint bitpattern = 1u << 2; // 1u << (int)(value - 1u)
bool value3IsInRow = ((maskForNumbersSetInRow[3] & bitpattern) != 0);

00000000 00000000 00000001 01001011 // the mask
                 |
00000000 00000000 00000000 00000100 // bitpattern for the value 3
                 =
00000000 00000000 00000000 00000000 // the result is 0. value 3 is not in the row.

以下是在棋盘中设置新值、更新适当的位掩码以及检查移动是否合法的方法。

public void insertNewValue(int row, int col, uint value)
{

    if(!isMoveLegal(row, col, value))
        throw ...

    theBoard[row, col] = value;

    uint bitpattern = 1u << (int)(value - 1u);

    maskForNumbersSetInRow[row] |= bitpattern;

    maskForNumbersSetInCol[col] |= bitpattern;

    int boxRowNumber = row / 3;
    int boxColNumber = col / 3;

    maskForNumbersSetInBox[boxRowNumber, boxColNumber] |= bitpattern;

}

有了面具,你可以像这样检查移动是否合法:

public bool isMoveLegal(int row, int col, uint value)
{

    uint bitpattern = 1u << (int)(value - 1u);

    int boxRowNumber = row / 3;
    int boxColNumber = col / 3;

    uint combinedMask = maskForNumbersSetInRow[row] | maskForNumbersSetInCol[col]
                        | maskForNumbersSetInBox[boxRowNumber, boxColNumber];

    return ((theBoard[row, col] == 0) && ((combinedMask & bitpattern) == 0u);
}

【讨论】:

  • 非常有趣的东西,但有点让我头疼。似乎在这里发生的事情可能需要一点点构建(一些带有位示例的 cmets)。构建掩码的位移等对我来说有点令人难以置信。只是问一下,因为您清楚地花了一些时间来回答。
【解决方案4】:

Dozens of bit twiddling examples here

代码在 C 中,但您可以轻松地将其改编为 C#

【讨论】:

    【解决方案5】:

    如果您需要与硬件通信,则需要在某些时候使用位旋转。

    提取像素值的 RGB 值。

    很多东西

    【讨论】:

      【解决方案6】:
      1. 它们可用于通过一个有限大小的变量将许多参数传递给函数。
      2. 优点是内存开销低或内存成本低:因此提高了性能。
      3. 我不能当场写教程,但我敢肯定他们已经在那儿了。

      【讨论】:

      • 哎呀,我刚才看到你标记了这个 C#,我还以为是 C++。 ...必须读慢... :)
      【解决方案7】:

      它们可以用于大量不同的应用程序,这是我之前在这里发布的一个问题,它使用按位运算:

      Bitwise AND, Bitwise Inclusive OR question, in Java

      对于其他示例,请查看(例如)标记的枚举。

      在我的示例中,我使用按位运算将二进制数的范围从 -128...127 更改为 0..255(将其表示从有符号更改为无符号)。

      这里是 MSN 文章 ->

      http://msdn.microsoft.com/en-us/library/6a71f45d%28VS.71%29.aspx

      很有用。

      而且,虽然这个链接:

      http://weblogs.asp.net/alessandro/archive/2007/10/02/bitwise-operators-in-c-or-xor-and-amp-amp-not.aspx

      非常技术性,它涵盖了所有内容。

      HTH

      【讨论】:

        【解决方案8】:

        只要您有 1 个或多个项目组合的选项,那么按位通常很容易解决。

        一些示例包括安全位(等待贾斯汀的样本..)、安排日期等。

        【讨论】:

          【解决方案9】:

          我不得不说最常见的用途之一是修改位域来压缩数据。您通常会在试图节省数据包的程序中看到这一点。

          network compression using bitfields 的示例

          【讨论】:

            【解决方案10】:

            我在 C# 中使用它们的最常见的事情之一是生成哈希码。有一些使用它们的相当好的散列方法。例如。对于具有 X 和 Y 的坐标类,我可能会使用这两个整数:

            public override int GetHashCode()
            {
              return x ^ ((y << 16) | y >> 16);
            }
            

            这会快速生成一个在由相等对象生成时保证相等的数字(假设相等意味着 X 和 Y 参数在比较的两个对象中相同),同时也不会为低值对象产生冲突模式(可能在大多数应用程序中最常见)。

            另一个是组合标志枚举。例如。 RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase

            当您针对 .NET 之类的框架进行编码时,通常不需要一些低级操作(例如,在 C# 中,我不需要编写代码来将 UTF-8 转换为 UTF-16,它就在那里在框架中对我来说),但当然有人必须编写该代码。

            有一些位旋转技术,例如四舍五入到最接近的二进制数(例如,将 1010 向上舍入到 10000):

                    unchecked
                    {
                        --x;
                        x |= (x >> 1);
                        x |= (x >> 2);
                        x |= (x >> 4);
                        x |= (x >> 8);
                        x |= (x >> 16);
                        return ++x;
                    }
            

            当你需要它们时它们很有用,但这种情况并不常见。

            最后,您还可以使用它们对数学进行微优化,例如 &lt;&lt; 1 而不是 * 2,但我只是说这通常是一个坏主意,因为它隐藏了真实代码的意图,几乎没有节省任何东西在性能方面,并且可以隐藏一些细微的错误。

            【讨论】:

            【解决方案11】:

            二进制排序。存在使用除法运算符而不是位移运算符的实现问题。这导致 BS 在集合大小超过 10,000,000 后失败

            【讨论】:

              【解决方案12】:

              您会出于各种原因使用它们:

              • 以内存高效的方式存储(和检查!)选项标志
              • 如果进行计算编程,您可能需要考虑使用按位运算而不是数学运算符来优化某些运算(注意副作用)
              • Gray Code!
              • 创建枚举值

              我相信你能想到其他人。

              话虽如此,有时您需要问自己:内存和性能提升是否值得付出努力。写完那种代码后,让它休息一会儿再回来。如果遇到困难,请使用更易于维护的代码重写。

              另一方面,有时使用按位运算确实很有意义(想想密码学)。

              更好的是,让其他人阅读并广泛记录。

              【讨论】:

                【解决方案13】:

                游戏!

                过去,我用它来代表黑白棋选手的棋子。它是 8X8,所以我需要一个 long 类型,然后,例如,如果你想知道所有棋子在哪里 - 你 or 两个玩家棋子。
                如果你想要一个玩家的所有可能的步骤,对右边说 - 你&gt;&gt;玩家的棋子表示为一个,然后AND它与对手的棋子一起检查现在是否有共同的1(这意味着有一个对手一块在你的右边)。然后你继续这样做。如果您回到自己的作品中-别动。如果您找到一个清晰的位置 - 您可以移动到那里,并在途中捕获所有碎片。
                (这种技术被广泛使用,在包括国际象棋在内的多种棋盘游戏中)

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-09-27
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-09-13
                  • 2011-07-14
                  • 1970-01-01
                  相关资源
                  最近更新 更多