C# 中的_BitScanForward?

Posted

技术标签:

【中文标题】C# 中的_BitScanForward?【英文标题】:_BitScanForward in C#? 【发布时间】:2012-02-01 02:48:50 【问题描述】:

我正在将一个用 C++ 编写的程序翻译成 C#,我遇到了一个无法解决的内在函数。在 C++ 中,这被称为:

unsigned char _BitScanForward(unsigned long * Index, unsigned long Mask);

如果我只知道内部函数所在的 DLL(如果有的话),我可以使用 P/Invoke。由于我不知道,我在 .NET 框架中寻找替代方案,但我空手而归。

有谁知道如何在 _BitScanForward 上使用 P/Invoke 或做同样事情的 .NET 方法?

感谢您的帮助,谢谢。

【参考方案1】:

内在函数不在任何库中,它们在 CPU 内部实现,编译器发出 machine code,CPU 将其识别为引发此特定行为。

它们是一种访问没有简单 C 等效指令的方法。

直到 .NET 优化器变得足够智能以识别它们(例如,Mono JIT 识别一些 SIMD 指令,在 MSIL 中编码为对特定类的函数的调用,类似地,.NET JIT 替换对 System.Math 方法的调用使用浮点运算),你的 C# 代码注定会比原来的 C++ 慢一个数量级。

【讨论】:

【参考方案2】:

_BitScanForward C++ 函数是一个内部编译器函数。它在从最低位到最高位搜索的字节序列中找到第一个 on 位并返回该位的值。您可能可以在 C# 中使用位操作策略来实现类似的东西(尽管它永远不会接近相同的性能)。如果您对 C++ 中的位操作感到满意,那么它在 C# 中基本相同。

【讨论】:

【参考方案3】:

_BitScanForward 搜索整数中的第一个设置位,从最低有效位开始搜索最高有效位。它在 x86 平台上编译为bsf instruction。

The bit twiddling hacks page 包含一些在不同情况下表现出色的潜在替换算法。有一个 O(N) 函数(具有均匀分布输入的一半时间只返回一次迭代)和一些次线性选项,还有一些利用乘法步骤。选择一个可能不是微不足道的,但任何一个都应该工作。

【讨论】:

【参考方案4】:

P/Invoke _BitScanForward 是不可能的,因为它是一个编译器内在函数,而不是一个实际的库函数(它被 Visual C++ 编译器翻译成 BSF x86 机器指令)。据我所知,这个“查找第一组”操作没有 MSIL 指令。最简单的做法是编写自己的 C++ 原生 DLL,导出一个调用 _BitScanForward() 的函数,然后 P/Invoke 该函数。

您也可以使用位操作直接在 C# 中编写它(请参阅Algorithms for find first set in Wikipedia)。我不确定这是否会比 P/Invoke 更快或更慢。测量并找出答案。

【讨论】:

Advice 与为什么这个函数成为固有函数完全相反......但 PInvoke 也可能没问题......【参考方案5】:

哇,最近的改进中似乎还有一个关于 C# 的问题。

其他评论者已经正确地指出,像 _BitScanForward 这样的内在函数本身并不是函数,而是编译器将特定平台指令注入目标代码的标记。用高级语言模拟一个内在函数是不可能的(除非你愿意付出抽象的代价)。 不过,好消息是,从 .Net Core 3.0 开始,JIT 确实支持许多硬件平台的内部函数。

对于 _BitScanForward,您可以使用 System.Runtime.Intrinsics.X86.Bmi1.TrailingZeroCount。

警告:不要忘记在使用前检查Bmi1.IsSupported,否则代码会在运行时失败。

您还可以通过使用它们的 ffs 内在函数在 ARM (.Net 5.0+) 上获得不错的执行速度:

public int ArmBitScanForward(int x)
  => 32 − System.Runtime.Intrinsics.Arm.ArmBase.LeadingZeroCount(x & −x);
public int ArmBitScanForward(long x)
  => 64 − System.Runtime.Intrinsics.Arm.ArmBase.Arm64.LeadingZeroCount(x & −x);

如果这两个平台都不存在,您将不得不求助于像 de-Bruijun 序列这样的小技巧:

for i from 0 to 31: table[ ( 0x077CB531 * ( 1 << i ) ) >> 27 ] ← i  // table [0..31] initialized
function ctz5 (x)
    return table[((x & -x) * 0x077CB531) >> 27]

(取自https://en.wikipedia.org/wiki/Find_first_set)

根据任务限制,我会在运行时选择不同的算法选择策略。每次调用都进行分支可能会扼杀所有效率。最有效的方法是在更高的级别上分支 - 即在运行时有三个版本的代码可供选择。 自动化代码生成的一种简单方法是使用位处理类型参数化您的代码:

public interface IBitScanner

  int BitScanForward(int x);


public int MyFunction<T>(int[] data)
  where T: new, IBitScanner

  var s=0;
  var scanner = new T(); 
  foreach(var i in data)
    s+= scanner.BitScanForward(i);
  return s;

然后我们定义几个结构来实现我们的扫描器:

public struct BitScannerX86: IBitScanner

   public int BitScanForward(int x)
     => unchecked((int)System.Runtime.Intrinsics.X86.Bmi1.TrailingZeroCount((uint)x));

public struct BitScannerArm: IBitScanner

   public int BitScanForward(int x)
     => 32 − System.Runtime.Intrinsics.Arm.ArmBase.LeadingZeroCount(x & −x);

public struct BitScanner: IBitScanner

  private static int[] _table = InitTable();
  private static int[] InitTable()
  
    var table = new int[32];
    for(var i=0; i<table.Length; i++)
      table[i] = ( 0x077CB531 * ( 1 << i ) ) >> 27;
    return table;
   
  public int BitScanForward(int x)
    => _table[((x & -x) * 0x077CB531) >> 27]

现在,每当我们需要特定于平台的 MyFunction 版本时,我们都会通过 MyFunction&lt;BitScannerArm&gt;。作为结构,类型参数强制 JIT 为其生成特定代码,而不是幻想虚拟调用的通用代码。 然后,由于 T 在 JIT 时间已知,对 BitScanForward 的调用被内联,并以适当的内在函数注入循环结束。 根据 MyFunction 任务的大小,这个版本的 MyFunction 可能会保存到委托中,成为接口的一部分,或者是实现接口的结构的一部分,以将技巧上一层重复。

请注意,最初的问题与跨平台兼容性无关,因为 _BitScanForward 是英特尔专用的指令。 在 C++ 世界中,针对特定的 OS&HW 组合编译可执行文件可能没问题;像 Java/.Net 这样的现代托管代码有机会在任何地方执行。

【讨论】:

以上是关于C# 中的_BitScanForward?的主要内容,如果未能解决你的问题,请参考以下文章

Unity人物行走卡顿,和方向问题

C# 中的 SQL Server 更新语句

c#中的_Default关键字是啥意思

Visual c# 中的 _stat 替代方案

c# 中的 _continue_ 关键字计算成本高吗?

C#获取C# DLL中的指定接口的所有实现实例 - qq_19759475的博客 - CSDN博客