_BitScanForward in C#?

_BitScanForward in C#?

本文关键字:in BitScanForward      更新时间:2023-10-16

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

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

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

有人知道如何在_BitScanForward上使用p/Invoke,或者一个做同样事情的.NET方法吗?

感谢您的帮助。

内部函数不在任何库中,它们在CPU内部实现,编译器发出机器代码,CPU将其识别为引发这种特定行为。

它们是一种访问指令的方式,这些指令没有简单的C等价物。

在.NET优化器变得足够聪明来识别它们之前(例如,Mono JIT识别一些SIMD指令,这些指令在MSIL中编码为对特定类函数的调用,类似地,.NET JIT用浮点运算取代了对System.Math方法的调用),您的C#代码的运行速度注定会比原始C++慢一个数量级。

哇,似乎有一个关于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_firstronget)

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

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版本时,我们都可以通过CCD_ 2。作为结构体,type参数强制JIT为其生成特定的代码,而不是幻想虚拟调用的泛型代码。然后,正如JIT时已知的T一样,对BitScanForward的调用被内联,并最终在循环中注入适当的内部函数。根据MyFunction任务的大小,此版本的MyFunction可能保存到委托、接口的一部分或实现接口的结构的一部分,以在更高级别上重复该技巧。

请注意,最初的问题并没有涉及跨平台兼容性,因为_BitScanForward是英特尔独有的指令。在针对特定OS&HW组合;像Java/.Net这样的现代托管代码有机会在任何地方执行。

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

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

这个"小技巧"页面包括一些在不同情况下都很出色的潜在替代算法。有一个O(N)函数(均匀分布输入的一半时间只需一次迭代即可返回)和一些亚线性选项,还有一些使用乘法步骤。挑选一个可能不是微不足道的,但任何一个都应该有效。

不可能p/Invoke_BitScanForward,因为它是编译器内部函数,而不是实际的库函数(它由Visual C++编译器转换为BSF x86机器指令)。据我所知,这个"查找第一集"操作没有MSIL指令。最简单的方法是编写自己的C++本机DLL,导出一个调用_BitScanForward()的函数,然后P/Invoke。

您也可以使用位操作直接在C#中编写它(请参阅维基百科中查找第一集的算法)。我不确定这会比P/Invoke快还是慢。测量并找出。