Skip to content

Releases: inversionhourglass/Pooling

v0.1.0

15 Oct 21:51
Compare
Choose a tag to compare

Pooling

中文 | English

Pooling是一个编译时对象池组件,通过在编译时将new操作替换为对象池操作,简化编码过程,无需开发人员手动编写对象池操作代码。同时提供了完全无侵入式的解决方案,可用作临时性能优化的解决方案和老久项目性能优化的解决方案等。

快速开始

引用Pooling.Fody

dotnet add package Pooling.Fody

确保FodyWeavers.xml文件中已配置Pooling,如果当前项目没有FodyWeavers.xml文件,可以直接编译项目,会自动生成FodyWeavers.xml文件:

<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Pooling /> <!--确保存在Pooling节点-->
</Weavers>
// 1. 需要池化的类型实现IPoolItem接口
public class TestItem : IPoolItem
{
    public int Value { get; set; }

    // 当对象返回对象池化时通过该方法进行重置实例状态
    public bool TryReset()
    {
        return true;
    }
}

// 2. 在任何地方使用new关键字创建该类型的对象
public class Test
{
    public void M()
    {
        var random = new Random();
        var item = new TestItem();
        item.Value = random.Next();
        Console.WriteLine(item.Value);
    }
}

// 编译后代码
public class Test
{
    public void M()
    {
        TestItem item = null;
        try
        {
            var random = new Random();
            item = Pool<TestItem>.Get();
            item.Value = random.Next();
            Console.WriteLine(item.Value);
        }
        finally
        {
            if (item != null)
            {
                Pool<TestItem>.Return(item);
            }
        }
    }
}

IPoolItem

正如快速开始中的代码所示,实现了IPoolItem接口的类型便是一个池化类型,在编译时Pooling会将其new操作替换为对象池操作,并在finally块中将池化对象实例返还到对象池中。IPoolItem仅有一个TryReset方法,该方法用于在对象返回对象池时进行状态重置,该方法返回false时表示状态重置失败,此时该对象将会被丢弃。

PoolingExclusiveAttribute

默认情况下,实现IPoolItem的池化类型会在所有方法中进行池化操作,但有时候我们可能希望该池化类型在部分类型中不进行池化操作,比如我们可能会创建一些池化类型的管理类型或者Builder类型,此时在池化类型上应用PoolingExclusiveAttribute便可指定该池化类型不在某些类型/方法中进行池化操作。

[PoolingExclusive(Types = [typeof(TestItemBuilder)], Pattern = "execution(* TestItemManager.*(..))")]
public class TestItem : IPoolItem
{
    public bool TryReset() => true;
}

public class TestItemBuilder
{
    private readonly TestItem _item;

    private TestItemBuilder()
    {
        // 由于通过PoolingExclusive的Types属性排除了TestItemBuilder,所以这里不会替换为对象池操作
        _item = new TestItem();
    }

    public static TestItemBuilder Create() => new TestItemBuilder();

    public TestItemBuilder SetXxx()
    {
        // ...
        return this;
    }

    public TestItem Build()
    {
        return _item;
    }
}

public class TestItemManager
{
    private TestItem? _cacheItem;

    public void Execute()
    {
        // 由于通过PoolingExclusive的Pattern属性排除了TestItemManager下的所有方法,所以这里不会替换为对象池操作
        var item = _cacheItem ?? new TestItem();
        // ...
    }
}

如上代码所示,PoolingExclusiveAttribute有两个属性TypesPatternTypesType类型数组,当前池化类型不会在数组中的类型的方法中进行池化操作;Patternstring类型AspectN表达式,可以细致的匹配到具体的方法(AspectN表达式格式详见:https://github.com/inversionhourglass/Shared.Cecil.AspectN/blob/master/README.md ),当前池化类型不会在被匹配到的方法中进行池化操作。两个属性可以使用其中一个,也可以同时使用,同时使用时将排除两个属性匹配到的所有类型/方法。

NonPooledAttribute

前面介绍了可以通过PoolingExclusiveAttribute指定当前池化对象在某些类型/方法中不进行池化操作,但由于PoolingExclusiveAttribute需要直接应用到池化类型上,所以如果你使用了第三方类库中的池化类型,此时你无法直接将PoolingExclusiveAttribute应用到该池化类型上。针对此类情况,可以使用NonPooledAttribute表明当前方法不进行池化操作。

public class TestItem1 : IPoolItem
{
    public bool TryReset() => true;
}
public class TestItem2 : IPoolItem
{
    public bool TryReset() => true;
}
public class TestItem3 : IPoolItem
{
    public bool TryReset() => true;
}

public class Test
{
    [NonPooled]
    public void M()
    {
        // 由于方法应用了NonPooledAttribute,以下三个new操作都不会替换为对象池操作
        var item1 = new TestItem1();
        var item2 = new TestItem2();
        var item3 = new TestItem3();
    }
}

有的时候你可能并不是希望方法里所有的池化类型都不进行池化操作,此时可以通过NonPooledAttribute的两个属性TypesPattern指定不可进行池化操作的池化类型。TypesType类型数组,数组中的所有类型在当前方法中均不可进行池化操作;Patternstring类型AspectN类型表达式,所有匹配的类型在当前方法中均不可进行池化操作。

public class Test
{
    [NonPooled(Types = [typeof(TestItem1)], Pattern = "*..TestItem3")]
    public void M()
    {
        // TestItem1通过Types不允许进行池化操作,TestItem3通过Pattern不允许进行池化操作,仅TestItem2可进行池化操作
        var item1 = new TestItem1();
        var item2 = new TestItem2();
        var item3 = new TestItem3();
    }
}

AspectN类型表达式灵活多变,支持逻辑非操作符!,所以可以很方便的使用AspectN类型表达式仅允许某一个类型,比如上面的示例可以简单改为[NonPooled(Pattern = "!TestItem2")],更多AspectN表达式说明,详见:https://github.com/inversionhourglass/Shared.Cecil.AspectN/blob/master/README.md

NonPooledAttribute不仅可以应用于方法层级,还可以应用于类型和程序集。应用于类等同于应用到类的所有方法上(包括属性和构造方法),应用于程序集等同于应用到当前程序集的所有方法上(包括属性和构造方法),另外如果在应用到程序集时没有指定TypesPattern两个属性,那么就等同于当前程序集禁用Pooling。

无侵入式池化操作

前面介绍的IPoolItem需要手动更改代码完成接入,适用于新项目接入及小型项目改造,对于庞大的老久项目,这样的改动可能牵扯甚广,考虑到可能带来的风险,可能就懒得改了。Pooling提供了无侵入式的接入方式,不需要实现IPoolItem接口,通过配置即可指定池化类型。

假设目前有如下代码:

namespace A.B.C;

public class Item1
{
    public object? GetAndDelete() => null;
}

public class Item2
{
    public bool Clear() => true;
}

public class Item3 { }

public class Test
{
    public static void M1()
    {
        var item1 = new Item1();
        var item2 = new Item2();
        var item3 = new Item3();
        Console.WriteLine($"{item1}, {item2}, {item3}");
    }

    public static async ValueTask M2()
    {
        var item1 = new Item1();
        var item2 = new Item2();
        await Task.Yield();
        var item3 = new Item3();
        Console.WriteLine($"{item1}, {item2}, {item3}");
    }
}

项目在引用Pooling.Fody后,编译项目时项目文件夹下会生成一个FodyWeavers.xml文件,我们按下面的示例修改Pooling节点:

<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Pooling>
    <Items>
      <Item pattern="A.B.C.Item1.GetAndDelete" />
      <Item pattern="Item2.Clear" inspect="execution(* Test.M1(..))" />
      <Item stateless="*..Item3" not-inspect="method(* Test.M2())" />
	</Items>
  </Pooling>
</Weavers>

上面的配置中,每一个Item节点匹配一个池化类型,上面的配置中展示了全部的四个属性,它们的含义分别是:

  • pattern: AspectN类型+方法表达式。匹配到的类型为池化类型,匹配到的方法为状态重置方法(等同于IPoolItem的TryReset方法)。需要注意的是,重置方法必须是无参的。
  • stateless: AspectN类型表达式。匹配到的类型为池化类型,该类型为无状态类型,不需要重置操作即可回到对象池中。
  • inspect: AspectN表达式。patternstateless匹配到的池化类型,只有在该表达式匹配到的方法中才会进行池化操作。当该配置缺省时表示匹配当前程序集的所有方法。
  • not-inspect: AspectN表达式。patternstateless匹配到的池化类型不会在该表达式匹配到的方法中进行池化操作。当该配置缺省时表示不排除任何方法。最终池化类型能够进行池化操作的方法集合为inspect集合与not-inspect集合的差集。

那么通过上面的配置,Test在编译后的代码为:

public class Test
{
    public static void M1()
    {
        Item1 item1 = null;
        Item2 item2 = null;
        Item3 item3 = null;
        try
        {
            item1 = Pool<Item1>.Get();
            item2 = Pool<Item2>.Get();
            item3 = Pool<Item3>.Get();
            Console.WriteLine($"{item1}, {item2}, {item3}");
        }
        finally
        {
            if (item1 != null)
            {
                item1.GetAndDelete();
                Pool<Item1>.Return(item1);
            }
            if (item2 != null)
            {
                if (item2.Clear())
                {
                    Pool<Item2>.Return(item2);
                }
            }
            if (item3 != null)
            {
                Pool<Item3>.Return(item3);
            }
        }
    }

    public static async ValueTask M2()
    {
        Item1 item1 = null;
        try
        {
            item1 = Pool<Item1>.Get();
            var item2 = new Item2();
            await Task.Yield();
            var item3 = new Item3();
            Console.WriteLine($"{item1}, {item2}, {item3}");
        }
        finally
        {
            if (item1 != null)
            {
                item1.GetAndDelete();
                Pool<Item1>.Return(item1);
            }
        }
    }
}

细心的你可能注意到在M1方法中,item1item2在重置方法的调用上有所区别,这是因为Item2的重置方法的返回值类型为bool,Poolinng会将其结果作为是否重置成功的依据,对于void或其他类型的返回值,Pooling将在方法成功返回后默认其重置成功。

零侵入式池化操作

看到标题是不是有点懵,刚介绍完无侵入式,怎么又来个零侵入式,它们有什么区别?

在上面介绍的无侵入式池化操作中,我们不需要改动任何C#代码即可完成指定类型池化操作,但我们仍需要添加Pooling.Fody的NuGet依赖,并且需要修改FodyWeavers.xml进行配置,这仍然需要开发人员手动操作完成。那如何让开发人员完全不需要任何操作呢?答案也很简单,就是将这一步放到CI流程或发布流程中完成。是的,零侵入是针对开发人员的,并不是真的什么都不需要做,而是将引用NuGet和配置FodyWeavers.xml的步骤延后到CI/发布流程中了。

优势是什么

类似于对象池这类型的优化往往不是仅仅某一个项目需要优化,这种优化可能是普遍性的,那么此时相比一个项目一个项目的修改,统一的在CI流程/发布流程中配置是更为快速的选择。另外在面对一些古董项目时,可能没有人愿意去更改任何代码,即使只是项目文件和FodyWeavers.xml配置文件,此时也可以通过修改CI/发布流程来完成。当然修改统一的CI/发布流程的影响面可能更广,这里只是提供一种零侵入式的思路,具体情况还需要结合实际情况综合考虑。

如何实现

最直接的方式就是在CI构建流程或发布流程中通过dotnet add package Pooling.Fody为项目添加NuGet依赖,然后将预先配置好的FodyWeavers.xml复制到项目目录下。但如果项目还引用了其他Fody插件,直接覆盖原有的FodyWeavers.xml可能导致原有的插件无效。当然,你也可以复杂点通过脚本控制FodyWeavers.xml的内容,这里我推荐一个.NET CLI工具,Cli4Fody可以一步完成NuGet依赖和FodyWeavers.xml配置。

<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Pooling>
    <Items>
      <Item pattern="A.B.C.Item1.GetAndDelete" />
      <Item pattern="Item2.Clear" inspect="execution(* Test.M1(..))" />
      <Item stateless="*..Item3" not-inspect="method(* Test.M2())" />
	</Items>
  </Pooling>
</Weavers>

上面的FodyWeavers.xml,使用Cli4Fody对应的命令为:

fody-cli MySolution.sln \
        --addin Pooling -pv 0.1.0 \
            -n Items:Item -a "pattern=A.B.C.Item1.GetAndDelete" \
            -n Items:Item -a "pattern=Item2.Clear" -a "inspect=execution(* Test.M1(..))" \
            -n Items:Item -a "stateless=*..Item3" -a "not-inspect=method(* Test.M2())"

Cli4Fody的优势是,NuGet引用和FodyWeavers.xml可以同时完成,并且Cli4Fody并不会修改或删除FodyWeavers.xml中其他Fody插件的配置。更多Cli4Fody相关配置,详见:https://github.com/inversionhourglass/Cli4Fody

Rougamo零侵入式优化案例

肉夹馍(Rougamo),一款静态代码编织...

Read more