2

我正在建模一些类来表示 C# 中的度量单位。例如,我对毫米和英寸进行了建模,其中有一个IDistanceUnit接口和一个DistanceUnit提供通用实现细节的基类。

在每个具体类中都有这样定义的基本算术函数:

public class Inches : 
    DistanceUnit<Inches>, 
    IDistanceUnit, 
    INumericUnit<Inches, IDistanceUnit>
{
    // ... snip ...
    public Inches Add(IDistanceUnit unit)
    {
        return new Inches(Value + unit.ToInches().Value);
    }
    // ... snip ...
}

对 10,000,000 次不同值转换为英寸和毫米的加法进行基准测试,对性能的影响很小但可以接受。手动执行转换的原始双打大约需要 70-100 毫秒,其中类大约需要 700-1000 毫秒。

我可以将细节抽象到DistanceUnit基类中并删除一些不必要的重复:

public abstract class DistanceUnit<TConcrete>
        IDistanceUnit,
        INumericUnit<TConcrete, IDistanceUnit>
    where TConcrete :
        IDistanceUnit,
        INumericUnit<TConcrete, IDistanceUnit>,
        new()
{
    // ... snip ...
    public TConcrete Add(IDistanceUnit unit)
    {
        TConcrete result = new TConcrete();
        reuslt.Value = Value + ToThis(unit).Value();
        return result;
    } 
    // ... snip ...
}
// ToThis() calls the correct "ToXYZ()" for the current implementing class

这使我的表现至少降低了 10 倍。与 800 毫秒相比,我看到大约 8000 毫秒,只是将实现移动到基类中。

我已经从手动测试中排除了一些事情:

  • ToThis(IDistanceUnit)当在具体类中使用而不是直接调用“ToInches”或“ToMillimeters”时,切换到 using没有显示出明显的性能影响
  • 使用无参数构造函数new TConcrete()

我正在使用以下代码对我的结果进行基准测试:

int size = 10000000;
double[] mms = new double[size];
double[] inches = new double[size];

// Fill in some arbitrary test values
for (int i = 0; i < size; i++)
{
    mms[i] = i;
    inches[i] = i;
}

Benchmark("Manual Conversion", () =>
{
    for (int i = 0; i < size; i++)
    {
        var result = mms[i] + (inches[i] * 25.4);
    }
});

Benchmark("Unit Classes", () => 
{
    for (int i = 0; i < size; i++)
    {
        var result = (new Millimeters(mms[i])) + (new Inches(inches[i]));
    }
}

Benchmark 只是在提供的 Action 周围调用秒表并以毫秒为单位打印出经过的时间。

唯一似乎造成重大差异的是Add函数从具体类到抽象基类的移动,但我不明白为什么我会因此而导致近 10 倍的性能下降。任何人都可以向我解释这一点(如果可能的话,建议一种方法来获得更好的速度而不诉诸代码重复)?

4

1 回答 1

3

对于我在评论中所说的第一个性能命中,我需要了解一些实现细节:ToInches() 方法、ToThis() 方法、Value 属性的类型或代码。

对于第二次性能命中,最可能的原因是您使用 new() 约束作为基类并使用
TConcrete result = new TConcrete();

编译器Activator.CreateInstance<TConcrete>()在这种情况下生成,这种方法在引擎盖下使用反射并且速度很慢。如果您有数百万个实例,它肯定会降低性能。在这里,您可以找到 Jon Skeet 的一些基准。

如果您使用 >= 3.5 框架,则可以使用表达式树在基类中构建对象的快速通用实例化:

private static Func<TConcrete> _createFunc;

    private static TConcrete CreateNew()
    {
            if (_func == null)
            {
                _createFunc = Expression.Lambda<Func<TConcrete>>(Expression.New(typeof (TConcrete))).Compile();
            }
            return _createFunc.Invoke();
    }

    public TConcrete Add(IDistanceUnit unit)
    {
            TConcrete result = CreateNew();
            result.Value = Value + ToThis(unit).Value();
            return result;
    } 
于 2013-06-04T16:21:28.280 回答