C#中的BitScanForward是什么?

9

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

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

如果我知道内置函数所在的DLL,我就可以使用P/Invoke。但是我不知道,因此我在.NET框架中寻找替代方法,但是没有找到。

有人知道如何在_BitScanForward上使用P/Invoke,或者有一个与之相同的.NET方法吗?

任何帮助都将不胜感激,谢谢。


请查看https://dev59.com/6HRA5IYBdhLWcg3w_DDF以获取更多有关此主题的评论。 - Alexei Levenkov
感谢所有回复的人。所有答案都非常有用并且回答了我的问题。 - SvalinnAsgard
5个回答

7
内在函数不在任何库中,它们是在 CPU 内部实现的,编译器会发出机器码,CPU 会将其识别为调用特定行为。
它们是一种获取访问没有简单 C 等效项的指令的方法。
直到 .NET 优化器变得足够聪明以识别它们(例如,Mono JIT 识别某些 SIMD 指令,编码为对特定类的函数调用的 MSIL,类似地,.NET JIT 将对 System.Math 方法的调用替换为浮点运算),否则您的 C# 代码注定比原始 C++ 运行慢一个数量级。

4

哇,看起来有一个关于C#的问题最近还没有被解决。

其他评论者已经正确地指出,像_BitScanForward这样的内置函数本质上不是函数,而是编译器将特定平台指令注入到目标代码中的标记。在高级语言中无法模拟内置函数(除非您愿意支付抽象代价)。 然而,好消息是,从.Net Core 3.0开始,JIT支持一些硬件平台的内置函数。

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

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

您还可以通过使用ARM的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<BitScannerArm> 来实现。作为结构体,类型参数强制JIT生成特定代码,而不是喜欢虚拟调用的通用代码。 然后,由于在JIT时间T已知,对BitScanForward的调用被内联,并最终注入到循环中适当的内部函数。 根据MyFunction任务的大小,此版本的MyFunction可能保存到委托中,成为接口的一部分,或成为实现接口的结构体的一部分,以将技巧重复一级。 请注意,原始问题并未考虑跨平台兼容性,因为_BitScanForward是仅限英特尔的指令。 这在C ++世界中编译可执行文件针对特定的OS&HW组合时可能是可以的;当代的受管理代码如Java / .Net有机会在任何地方执行。

3

_BitScanForward C++函数是一种内置编译器函数。它从最低位到最高位搜索一系列字节中的第一个“开启”位,并返回该位的值。您可以使用C#中的位操作技巧实现类似的功能(尽管性能不会接近于相同)。如果您熟悉C++中的位操作,则在C#中基本上是相同的。


3

_BitScanForward函数从最低位开始向最高位搜索,寻找整数中第一个置位(即二进制下为1的位置)。在x86平台上,它会被编译成bsf指令

位操作技巧页面包含了一些在不同情况下表现优异的潜在替代算法。其中有一个O(N)的函数(在均匀分布的输入情况下,一半的时间只需要一次迭代),还有一些次线性的选项,以及一些利用乘法步骤的算法。选择哪一个并不是轻松的决定,但任何一个都应该有效。


2
无法使用P/Invoke _BitScanForward,因为它是编译器内置函数,而不是实际的库函数(它由Visual C++编译器转换为BSF x86机器指令)。据我所知,没有MSIL指令可以执行此“查找第一个设置”的操作。最简单的方法是编写自己的C++本机DLL,导出调用_BitScanForward()的函数,然后进行P/Invoke。
您还可以直接使用位操作在C#中编写它(请参见维基百科上的查找第一个集合算法)。我不确定这是否比P/Invoke更快或更慢。测量并找出答案。

建议有点完全相反,与此函数的内在目的不符...但 PInvoke 也许也可以... - Alexei Levenkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接