【问题标题】:C# - Construct a signal Vector<T> from an integer bitmaskC# - 从整数位掩码构造信号 Vector<T>
【发布时间】:2022-01-22 21:54:42
【问题描述】:

我有一些表示位掩码的整数值,例如 154 = 0b10011010,我想构造一个对应的信号 Vector&lt;T&gt; 实例 &lt;0, -1, 0, -1, -1, 0, 0, -1&gt;

肯定有比这更有效的方法吗?

int mask = 0b10011010;// 154
// -1 = (unsigned) 0xFFFFFFFF is the "true" value
Vector<int> maskVector = new Vector<int>(
    Enumerable.Range(0, Vector<int>.Count)
        .Select(i => (mask & (1 << i)) > 0 ? -1 : 0)
        .ToArray());
// <0, -1, 0, -1, -1, 0, 0, -1>
string maskVectorStr = string.Join("", maskVector);

注意the debugger is bugged 显示Vector&lt;T&gt; 值,仅显示一半组件,其余部分为零,因此我使用string.Join

此外,在使用通用 Vector&lt;T&gt; 版本时我该如何做到这一点?

ConditionalSelect 的文档明确指出,掩码向量对于每个重载都有整数值,但是发送垃圾邮件 Vector&lt;T&gt;.Zero[0]Vector&lt;T&gt;.One[0] 来获取它们肯定是不合适的吗? (您可以使用(-Vector&lt;T&gt;.One)[0] 获得-1 的T 版本)


信号向量或整数掩码向量与ConditionalSelect 方法一起用于在其他两个掩码的值之间进行选择:

//powers of two <1, 2, 4, 8, 16, 32, 64, 128>
Vector<int> ifTrueVector = new Vector<int>(Enumerable.Range(0, Vector<int>.Count).Select(i => 1 << i).ToArray());
Vector<int> ifFalseVector = Vector<int>.Zero;// or some other vector
// <0, 2, 0, 8, 16, 0, 0, 128>
Vector<int> resultVector = Vector.ConditionalSelect(maskVector, ifTrueVector, ifFalseVector);
string resultStr = string.Join("", resultVector);
// our original mask value back
int sum = Vector.Dot(resultVector, Vector<int>.One);

附注是否也有相应的解决方案来填充 2 的幂?

【问题讨论】:

  • 你是如何从0b10011010&lt;0, -1, 0, -1, -1, 0, 0, -1&gt; 的?
  • 颠倒顺序。最低有效位是第一个向量分量。
  • (请注意,您的ConditionalSelect 可以使用&amp;,我认为?这意味着不需要ifFalseVectorSee the implementation
  • 我看不出你的代码有什么问题。该操作需要循环16次。
  • 你能用System.Runtime.Intrinsics.X86吗?如果是这样,有 x86 特定的有效解决方案

标签: c# vector simd intrinsics bitmask


【解决方案1】:

可能有一种基于向量的奇特方式来生成掩码向量,但只需优化当前代码即可将速度提高一个数量级以上。

首先,不要在热路径上使用 Linq。如果您正在寻找速度,那么中间对象分配、虚拟方法调用和委托调用的数量是不必要的。您可以将其重写为 for 循环,而不会丢失清晰度。

其次,摆脱数组分配。 Vector&lt;T&gt; 具有采用 Span&lt;T&gt; 的构造函数,您可以使用其中的 stackalloc 之一。

这给了你一些看起来有点像这样的代码:

int mask = 0b10011010;

Span<int> values = stackalloc int[Vector<int>.Count];
for (int i = 0; i < Vector<int>.Count; i++)
{
    values[i] = (mask & (1 << i)) > 0 ? -1 : 0;
}

var maskVector = new Vector<int>(values);

有趣的是,手动展开该循环会给您另一个显着的加速:

Span<int> values = stackalloc int[Vector<int>.Count];
values[0] = (mask & 0x1) > 0 ? -1 : 0;
values[1] = (mask & 0x2) > 0 ? -1 : 0;
values[2] = (mask & 0x4) > 0 ? -1 : 0;
values[3] = (mask & 0x8) > 0 ? -1 : 0;
values[4] = (mask & 0x10) > 0 ? -1 : 0;
values[5] = (mask & 0x20) > 0 ? -1 : 0;
values[6] = (mask & 0x40) > 0 ? -1 : 0;
values[7] = (mask & 0x80) > 0 ? -1 : 0;

var maskVector = new Vector<int>(values);

这表现如何?让我们使用BenchmarkDotNet

[MemoryDiagnoser]
public class MyBenchmark
{
    [Benchmark, Arguments(0b10011010)]
    public Vector<int> Naive(int mask)
    {
        Vector<int> maskVector = new Vector<int>(
            Enumerable.Range(0, Vector<int>.Count)
                .Select(i => (mask & (1 << i)) > 0 ? -1 : 0)
                .ToArray());

        return maskVector;
    }

    [Benchmark, Arguments(0b10011010)]
    public Vector<int> Optimised(int mask)
    {
        Span<int> values = stackalloc int[Vector<int>.Count];
        for (int i = 0; i < Vector<int>.Count; i++)
        {
            values[i] = (mask & (1 << i)) > 0 ? -1 : 0;
        }

        var output = new Vector<int>(values);
        return output;
    }

    [Benchmark, Arguments(0b10011010)]
    public Vector<int> Optimised2(int mask)
    {
        Span<int> values = stackalloc int[Vector<int>.Count];
        values[0] = (mask & 0x1) > 0 ? -1 : 0;
        values[1] = (mask & 0x2) > 0 ? -1 : 0;
        values[2] = (mask & 0x4) > 0 ? -1 : 0;
        values[3] = (mask & 0x8) > 0 ? -1 : 0;
        values[4] = (mask & 0x10) > 0 ? -1 : 0;
        values[5] = (mask & 0x20) > 0 ? -1 : 0;
        values[6] = (mask & 0x40) > 0 ? -1 : 0;
        values[7] = (mask & 0x80) > 0 ? -1 : 0;

        var output = new Vector<int>(values);
        return output;
    }
}

public class Program
{
    public static void Main()
    {
        var summary = BenchmarkRunner.Run<MyBenchmark>();
    }
}

这给出了结果:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1415 (21H2)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.101
  [Host]     : .NET 5.0.0 (5.0.20.51904), X64 RyuJIT
  DefaultJob : .NET 5.0.1 (5.0.120.57516), X64 RyuJIT
Method mask Mean Error StdDev Gen 0 Allocated
Naive 154 103.018 ns 2.0509 ns 4.0001 ns 0.0554 232 B
Optimised 154 13.405 ns 0.3004 ns 0.4497 ns - -
Optimised2 154 9.668 ns 0.2827 ns 0.8245 ns - -

【讨论】:

  • 当我希望有一些明显的或特殊用途的功能可以直接完成,但也可以很好地加速!
  • @Elaskanator 原来展开那个循环可以让你获得另一个不错的加速
  • 有没有希望将其推广到通用的Vector&lt;T&gt; 而不仅仅是int?如果Vector&lt;int&gt;.Count 的值一天不是 8 怎么办?
  • @Elaskanator 是的,这就是展开循环失败的地方。但是,如果有一天它减少了,你的代码就已经有麻烦了。我假设您正在考虑让T 成为另一种整数类型?像这样的东西? dotnetfiddle.net/6zwjXW
【解决方案2】:

可以使用Vector API 提供的向量运算对其进行补充,并且可以对任何T 进行等效变体,例如T = int

Vector<int> powersOfTwo;

Vector<int> MaskToElements(int mask)
{
    Vector<int> broadcasted = new Vector<int>(mask);
    Vector<int> singlebit = Vector.BitwiseAnd(broadcasted, powersOfTwo);
    return Vector.Equals(singlebit, powersOfTwo);
}

powersOfTwo 是这样创建的:

int[] powersOfTwoArray = new int[Vector<int>.Count];
for (int i = 0; i < powersOfTwoArray.Length; i++)
{
    powersOfTwoArray[i] = 1 << i;
}
powersOfTwo = new Vector<int>(powersOfTwoArray);

它并不是真正的通用,即使它适用于不同的类型,主要是因为powersOfTwo 必须预先计算,否则这个函数将没有任何意义:如果其中有一个标量循环,那有什么意义。


如果掩码是常量,那么通过System.Runtime.Intrinsics.X86 API可以直接进入Blend,不需要先转成向量掩码,例如:

Vector128.AsInt32(Sse41.Blend(Vector128.AsSingle(a), Vector128.AsSingle(b), (byte)mask));

如果掩码不是常量,该 API 仍会接受它,但最终会调用缓慢的回退。在这种情况下,最好制作一个矢量蒙版并使用BlendVariable

【讨论】:

猜你喜欢
  • 2015-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-06
相关资源
最近更新 更多