您当前的位置: 首页 > 

寒冰屋

暂无认证

  • 4浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

使用Span T 提高C#代码的性能

寒冰屋 发布时间:2020-06-26 22:10:47 ,浏览量:4

目录

介绍

Span实现

Ref返回

Ref结构

Span实现

Span限制

重写现有代码库以使用Span

评价

结论

 

 

介绍

以我的经验,提高应用程序性能的主要步骤是减少IO调用的次数和持续时间。但是,一旦执行了此选项,开发人员所采取的另一条路径就是使用堆栈上的内存。堆栈允许非常快速的分配和重新分配,尽管由于堆栈大小很小,堆栈仅应用于分配较小的部分。另外,使用堆栈可以减少GC的压力。为了在堆栈上分配内存,一种方法是将值类型或stackalloc运算符与非托管内存的使用结合使用。

由于用于非托管内存访问的API非常冗长,因此开发人员很少使用第二个选项。

Span是C#7.2中提供的一组值类型,它是来自不同来源的内存的无分配表示形式。Span允许开发人员以更方便的方式使用连续内存区域,从而确保内存和类型安全。

Span实现 Ref返回

Span对于那些不密切关注C#语言更新的用户,实现的第一步是了解C#7.0中引入的ref返回。

虽然大多数读者都熟悉按引用传递方法参数,但是现在C#允许返回对值的引用,而不是值本身。

让我们检查一下它是如何工作的。我们将围绕着一系列杰出的音乐家创建一个简单的包装,既展示传统行为又展示新的参考返回功能。

public class ArtistsStore
{
    private readonly string[] _artists = 
            new[] { "Amenra", "The Shadow Ring", "Hiroshi Yoshimura" };

    public string ReturnSingleArtist()
    {
        return _artists[1];
    }

    public ref string ReturnSingleArtistByRef()
    {
        return ref _artists[1];
    }

    public string AllAritsts => string.Join(", ", _artists);
}

现在让我们调用这些方法

var store = new ArtistsStore();
var artist = store.ReturnSingleArtist();
artist = "Henry Cow";
var allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura

artist = store.ReturnSingleArtistByRef();
artist = "Frank Zappa";
allArtists = store.AllAritsts;     //Amenra, The Shadow Ring, Hiroshi Yoshimura

ref var artistReference = ref store.ReturnSingleArtistByRef();
artistReference = "Valentyn Sylvestrov";
allArtists = store.AllAritsts;     //Amenra, Valentyn Sylvestrov, Hiroshi Yoshimura

请注意,在第一个和第二个示例中,原始集合未更改,而在最后一个示例中,我们设法更改了该集合的第二个艺术家。如您在本文后面所看到的那样,此有用的功能将帮助我们以类似引用的方式操作位于堆栈上的数组。

Ref结构

众所周知,值类型可能在堆栈上分配。同样,它们不一定取决于使用该值的上下文。为了确保始终在堆栈上分配该值,在C#7.0中引入了ref struct的概念。Span是一个ref struct,因此我们确定它总是分配在堆栈上。

Span实现

Span是一个ref struct,其中包含指向内存的指针和跨度的长度,如下所示。

public readonly ref struct Span
{
  private readonly ref T _pointer;
  private readonly int _length;
  public ref T this[int index] => ref _pointer + index;
  ...
}

注意指针字段附近的ref修饰符。不能在.NET Core的普通C#中声明这种构造,而是通过ByReference来实现的。

如您所见,索引是通过ref return实现的,它允许只支持堆栈的struct使用类似引用类型的行为。

Span限制

为了确保ref struct总是在堆栈使用的,它具有许多局限性,即,包括它们不能被装箱,它们不能被分配给object,dynamic类型的变量或任何接口类型,它们不能是引用类型字段,并且它们不能跨await和yield边界使用。另外,调用Equals和GetHashCode两个方法,并抛出NotSupportedException。Span是一个ref struct。

重写现有代码库以使用Span

让我们研究一下将Linux权限转换为八进制表示形式的代码。您可以在此处访问它。这是原始代码:

internal class SymbolicPermission
{
    private struct PermissionInfo
    {
        public int Value { get; set; }
        public char Symbol { get; set; }
    }

    private const int BlockCount = 3;
    private const int BlockLength = 3;
    private const int MissingPermissionSymbol = '-';

    private readonly static Dictionary Permissions = 
                                       new Dictionary() {
            {0, new PermissionInfo {
                Symbol = 'r',
                Value = 4
            } },
            {1, new PermissionInfo {
                Symbol = 'w',
                Value = 2
            }},
            {2, new PermissionInfo {
                Symbol = 'x',
                Value = 1
            }} };

    private string _value;

    private SymbolicPermission(string value)
    {
        _value = value;
    }

    public static SymbolicPermission Parse(string input)
    {
        if (input.Length != BlockCount * BlockLength)
        {
            throw new ArgumentException
                  ("input should be a string 3 blocks of 3 characters each");
        }
        for (var i = 0; i < input.Length; i++)
        {
            TestCharForValidity(input, i);
        }

        return new SymbolicPermission(input);
    }

    public int GetOctalRepresentation()
    {
        var res = 0;
        for (var i = 0; i < BlockCount; i++)
        {
            var block = GetBlock(i);
            res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
        }
        return res;
    }

    private static void TestCharForValidity(string input, int position)
    {
        var index = position % BlockLength;
        var expectedPermission = Permissions[index];
        var symbolToTest = input[position];
        if (symbolToTest != expectedPermission.Symbol && 
                            symbolToTest != MissingPermissionSymbol)
        {
            throw new ArgumentException($"invalid input in position {position}");
        }
    }

    private string GetBlock(int blockNumber)
    {
        return _value.Substring(blockNumber * BlockLength, BlockLength);
    }

    private int ConvertBlockToOctal(string block)
    {
        var res = 0;
        foreach (var (index, permission) in Permissions)
        {
            var actualValue = block[index];
            if (actualValue == permission.Symbol)
            {
                res += permission.Value;
            }
        }
        return res;
    }
}

public static class SymbolicUtils
{
    public static int SymbolicToOctal(string input)
    {
        var permission = SymbolicPermission.Parse(input);
        return permission.GetOctalRepresentation();
    }
}

推理非常简单:string是一个char数组,所以为什么不将其分配在堆栈上而不是堆上。

因此,我们的第一个目标是标记SymbolicPermission的_value字段作为ReadOnlySpan,而不是string。为此,我们必须声明SymbolicPermission为ref struct字段或属性不能是Span类型,除非它是ref struct的实例。

internal ref struct SymbolicPermission
{
    ...
    private ReadOnlySpan _value;
}

现在我们只需将我们所能到达的每个string更改为ReadOnlySpan。惟一感兴趣的是GetBlock方法,因为在这里我们用Slice替换了Substring。

private ReadOnlySpan GetBlock(int blockNumber)
{
    return _value.Slice(blockNumber * BlockLength, BlockLength);
}
评价

让我们来衡量结果:

我们注意到速度提高了50纳秒,大约是性能提高了10%。可以说50纳秒不算什么,但是实现它几乎不花时间!

现在,我们将评估具有18个块(每个字符12个字符)的权限上的改进,以查看我们是否可以获得显着的改进。

如您所见,我们设法获得了0.5微秒或5%的性能提升。同样,它看起来像是一个微不足道的成就。但是记住,这是很容易实现的。

结论

Span提供了一种安全且易于使用的替代方案stackallock,其可轻松提高性能。虽然从每次使用中获得的收益相对较小,但始终如一的使用却可以避免因数千次切割而导致的死亡。Span在.NET Core 3.0代码库中广泛使用,与以前的版本相比,它可以提高性能。

关注
打赏
1665926880
查看更多评论
立即登录/注册

微信扫码登录

0.1329s