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<BitScannerArm>
。作为结构,类型参数强制 JIT 为其生成特定代码,而不是幻想虚拟调用的通用代码。
然后,由于 T 在 JIT 时间已知,对 BitScanForward 的调用被内联,并以适当的内在函数注入循环结束。
根据 MyFunction 任务的大小,这个版本的 MyFunction 可能会保存到委托中,成为接口的一部分,或者是实现接口的结构的一部分,以将技巧上一层重复。
请注意,最初的问题与跨平台兼容性无关,因为 _BitScanForward 是英特尔专用的指令。 在 C++ 世界中,针对特定的 OS&HW 组合编译可执行文件可能没问题;像 Java/.Net 这样的现代托管代码有机会在任何地方执行。
【讨论】:
以上是关于C# 中的_BitScanForward?的主要内容,如果未能解决你的问题,请参考以下文章