Skip to content

性能优化

ihourglass edited this page Dec 16, 2024 · 5 revisions

性能优化

切面类型生命周期

Rougamo 5.0 之后,可以通过LifetimeAttribute指定切面类型的生命周期:

[Lifetime(Lifetime.Transient)] // 临时,每次创建都是直接new。在没有应用LifetimeAttribute时,默认为Transient
public class Test1Attribute : MoAttribute { }

[Lifetime(Lifetime.Pooled)] // 对象池,每次创建都从对象池中获取
public class Test2Attribute : MoAttribute { }

[Lifetime(Lifetime.Singleton)] // 单例
public class Test3Attribute : MoAttribute { }

需要注意的是,不是所有切面类型无脑设置为对象池或单例模式即可完成优化。选择生命周期时需要注意以下几点:

  1. 推荐程度:单例 > 对象池 > 临时

  2. 单例要求切面类型必须是无状态的,必须包含无参构造方法,且应用切面类型时不可调用有参构造方法或设置属性

    [Lifetime(Lifetime.Singleton)]
    public class SingletonAttribute : MoAttribute
    {
        public SingletonAttribute() { }
    
        public SingletonAttribute(int value) { }
    
        public int X { get; set; }
    }
    
    [Singleton(1)]     // 编译时报错,不可调用有参构造方法
    [Singleton(X = 2)] // 编译时报错,不可设置属性
  3. 对象池要求切面类型必须包含无参构造方法,且应用切面类型时不可调用有参构造方法。如果切面类型有状态可实现Rougamo.IResettable接口,并在TryReset方法中完成状态重置,或在OnExit / OnExitAsync中完成状态重置

    [Lifetime(Lifetime.Pooled)]
    public class PooledAttribute : MoAttribute, IResettable
    {
        public SingletonAttribute() { }
    
        public SingletonAttribute(int value) { }
    
        public int X { get; set; }
    
        public override void OnExit(MethodContext context)
        {
            // 可以在OnExit中状态重置,比如将X重置为0
            X = 0;
        }
    
        public bool TryReset()
        {
            // 也可以实现IResettable接口,在该方法中完成状态重置
            X = 0;
    
            // 返回true表示重置成功,返回false,当前对象将会直接抛弃,不会返回到对象池中
            return true;
        }
    }
    
    [Pooled(1)]     // 编译时报错,不可调用有参构造方法
    [Pooled(X = 2)] // 支持的操作,所以如果需要在应用时设置一些状态,可以用属性的方式而不要用构造方法参数的方式
  4. 结构体不支持自定义生命周期

总结来说,如果可以将切面类型设计为无状态的,推荐使用单例;如果无法保证无状态,但可以管理好状态,推荐使用对象池;如果无法很好的管理状态,还可以使用结构体;最后,如果对GC优化要求没那么严格,使用默认的临时策略即可。

部分编织

Rougamo有四个切面方法可以重写OnEntryOnSucessOnExceptionOnExit,分别对应着方法生命周期的四个节点,同时在对应的生命周期节点可以做一些操作来控制方法执行。但实际上,我们一般用不上这所有的功能,比如我们为参数设置默认值只用得上OnEntry和修改方法参数,再比如我们在方法异常时记录异常并返回特定值只用得上OnException和异常处理。

通过部分编织,我们可以仅选择自己需要的功能,减少织入的代码。默认是功能全启用的,可以通过Features属性来设置,Features属性的类型为枚举,下表列出了该枚举的所有项:

枚举值 功能
All 包含全部功能,默认值
OnEntry 仅OnEntry,不可修改参数值,不可修改返回值
OnException 仅OnException,不可处理异常
OnSuccess 仅OnSuccess,不可修改返回值
OnExit OnExit
RewriteArgs 包含OnEntry,同时可以在OnEntry中修改参数值
EntryReplace 包含OnEntry,同时可以在OnEntry中修改返回值
ExceptionHandle 包含OnException,同时可以在OnEntry中处理异常
SuccessReplace 包含OnSuccess,同时可以在OnSuccess中修改返回值
ExceptionRetry 包含OnException,同时可以在OnException中进行重试
SuccessRetry 包含OnSuccess,同时可以在OnSuccess中进行重试
Retry 包含OnException和OnSuccess,同时可以在OnException和OnSuccess中进行重试
Observe 包含OnEntry、OnException、OnSuccess和OnExit,常用于日志、APM埋点等操作
NonRewriteArgs 包含除修改参数外的所有功能
NonRetry 包含除重试外的所有功能
FreshArgs 在执行OnException、OnSuccess和OnExit前更新MethodContext.Arguments

结构体

肉夹馍是一个针对方法的AOP组件,对于应用了肉夹馍切面类型的方法,每次调用方法都会创建对应的切面类型实例和MethodContext对象,这些类实例在方法调用完成后将会进行GC,所以Rougamo的使用必然会增加GC的负担,这是不可避免的,即时我们手写AOP代码也会如此,但我们可以通过各种方式减少GC的压力,使用结构体代替类便是其中一种方式,因为结构体存储在栈中,不会进入GC。

定义结构体时,我们无法再直接继承MoAttribute等类型了,需要直接实现IMo接口。同样的,由于是结构体,我们也无法继承Attribute,如果希望将结构体像Attribute那样直接应用到方法或类或程序集上,可以通过RougamoAttribute实现。同时在这里还要再次推荐实现空接口IRougamo的方式。

struct ValueMo : IMo
{
    // 实现接口,定义AOP操作
}

[Rougamo(typeof(ValueMo))]
class Cls : IRougamo<ValueMo>
{
    // 如果项目使用C#11及以上语法,可以直接使用下面这种泛型Attribute
    [Rougamo<ValueMo>]
    public void M() { }
}

瘦身MethodContext

正如在 结构体 中说的那样,除了肉夹馍切面类型外,每次调用应用了肉夹馍切面类型的方法还会创建一个MethodContext对象,用于保存上下文信息。考虑到MethodContext对象要在多个肉夹馍切面类型中多次传递,而值类型每次传递都需要做一次浅拷贝,所以MethodContext并不适合设计为结构体。那么能够对MethodContext做的优化便是瘦身了。

MethodContext能够瘦身的地方主要包含三个部分:

  1. 保存当前方法所有肉夹馍切面类型实例的Mos属性;
  2. 保存方法参数列表的Arguments属性;
  3. 保存方法返回值的ReturnValue属性。

如果确定不需要某些上下文信息,在定义肉夹馍切面类型时可以通过MethodContextOmits进行配置,但有两点需要注意:

  1. 如果一个方法上应用了多个肉夹馍切面类型,MethodContextOmits的值取交集,也就是要想瘦身某一部分,必须所有切面类型都同意;

  2. 部分编织 中有介绍到FeaturesMethodContextOmits存在功能交叉,优先级上MethodContextOmits高于Features,下表是MethodContextOmits各枚举值对Features的影响:

    ✅表示MethodContextOmits设置了对应的值对该功能不影响,❌表示无法使用

    Mos Arguments ReturnValue
    修改方法参数值
    方法执行拦截
    修改方法返回值
    处理方法异常
    重试执行方法
    记录方法返回值
    刷新方法参数

强制同步

异步切面 中有介绍到“同步方法调用同步切面,异步方法调用异步切面”,这个是默认操作,但在异步方法中执行异步切面并不一定是最佳选择。如果你的异步切面方法的实现是直接调用同步切面方法,然后返回一个ValueTask默认值,那么在异步方法中调用异步切面的开销将大于直接调用同步切面。所以如果你希望你的某个或某些切面方法在异步方法中调用同步切面,那么就可以通过设置ForceSync属性强制调用同步切面:

// 在异步方法中,OnEntry和OnExit将强制调用同步切面方法
[Optimization(ForceSync = ForceSync.OnEntry | ForceSync.OnExit)]
public class TestAttribute : MoAttribute
{
}

自定义切面类型声明周期

结构体虽然可以避免创建引用类型,但结构体本身也存在很多限制,比如无法继承父类实现逻辑复用,无法继承 Attribute 导致无法在应用切面类型时指定参数(Attribute 可以在应用时指定构造方法参数和属性参数[Xyz(123, V = "abc")])等。兼顾易用性和性能的方式便是自定义声明周期。

[Lifetime(Lifetime.Transient)] // 临时,每次创建都是直接new。在没有应用LifetimeAttribute时,默认为Transient
public class Test1Attribute : MoAttribute { }

[Lifetime(Lifetime.Pooled)] // 对象池,每次创建都从对象池中获取
public class Test2Attribute : MoAttribute { }

[Lifetime(Lifetime.Singleton)] // 单例
public class Test3Attribute : MoAttribute { }

需要注意的是,不是所有切面类型无脑设置为对象池或单例模式即可完成优化。选择生命周期时需要注意以下几点:

  1. Singleton要求切面类型必须是无状态的,必须包含无参构造方法,且应用切面类型时不可调用有参构造方法或设置属性

    [Lifetime(Lifetime.Singleton)]
    public class SingletonAttribute : MoAttribute
    {
        public SingletonAttribute() { }
    
        public SingletonAttribute(int value) { }
    
        public int X { get; set; }
    }
    
    [Singleton(1)]     // 编译时报错,不可调用有参构造方法
    [Singleton(X = 2)] // 编译时报错,不可设置属性
  2. Pooled要求切面类型必须包含无参构造方法,且应用切面类型时不可调用有参构造方法。如果切面类型有状态可实现Rougamo.IResettable接口,并在TryReset方法中完成状态重置,或在OnExit / OnExitAsync中完成状态重置

    [Lifetime(Lifetime.Pooled)]
    public class PooledAttribute : MoAttribute, IResettable
    {
        public SingletonAttribute() { }
    
        public SingletonAttribute(int value) { }
    
        public int X { get; set; }
    
        public override void OnExit(MethodContext context)
        {
            // 可以在OnExit中状态重置,比如将X重置为0
            X = 0;
        }
    
        public bool TryReset()
        {
            // 也可以实现IResettable接口,在该方法中完成状态重置
            X = 0;
    
            // 返回true表示重置成功,返回false,当前对象将会直接抛弃,不会返回到对象池中
            return true;
        }
    }
    
    [Pooled(1)]     // 编译时报错,不可调用有参构造方法
    [Pooled(X = 2)] // 支持的操作,所以如果需要在应用时设置一些状态,可以用属性的方式而不要用构造方法参数的方式
  3. 结构体切面类型无法定义生命周期

总结来说,如果可以将切面类型设计为无状态的,推荐使用Singleton;如果无法保证无状态,但可以管理好状态的重置,推荐使用Pooled;如果无法很好的管理状态,可以使用结构体(但结构体无法继承 Attribute,所以无法在应用时像 Attribute 那样通过构造参数和属性动态配置);最后,如果对 GC 优化要求没那么严格,使用默认的无拘无束的Transient即可。