转换'编译时'类型的最快方法是什么?

时间:2012-01-21 16:24:17

标签: c# performance casting unsafe

我知道标题有点模糊。但我想要实现的是这样的:

在抽象类中:

public abstract bool TryGet<T>(string input, out T output) where T : struct;

在具有此签名的类中:

private class Param<T> : AbstractParam where T : struct

此实施:

public override bool TryGetVal<TOriginal>(string input, out TOriginal output)
{
    T oTemp;
    bool res = _func(input, out oTemp); // _func is the actual function
                                        // that retrieves the value.
    output = (TOriginal)oTemp; // Compile-time error
    return res;
}

TOriginal将始终与T的类型相同。这会绕过编译时错误,但我不想这样做性能打击:

output = (TOriginal)(object)oTemp;

如果它是引用类型,则会提供解决方案:

output = oTemp as TOriginal;

反射/动态也可以解决问题,但性能影响更大:

output = (TOriginal)(dynamic)oTemp;

我尝试使用不安全的代码,但没有成功,但那可能只是我。

所以我最好的希望是编译器优化(TOriginal)(object)oTemp(TOriginal)oTemp我不知道。或者说这是一个不安全的方法。

请告诉我关于过早优化的讲座,我想知道这纯粹是为了研究,如果有办法克服这个限制,我感兴趣。我意识到这对实际表现的影响可以忽略不计。

最终结论:
在拆解情况后,结果如下:

                return (TOut)(object)_value;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        eax 
00000004  mov         dword ptr [ebp-4],ecx 
00000007  cmp         dword ptr ds:[003314CCh],0 
0000000e  je          00000015 
00000010  call        61A33AD3 
00000015  mov         eax,dword ptr [ebp-4] 
00000018  mov         eax,dword ptr [eax+4] 
0000001b  mov         esp,ebp 
0000001d  pop         ebp 
0000001e  ret 

                return _value;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        eax 
00000004  mov         dword ptr [ebp-4],ecx 
00000007  cmp         dword ptr ds:[004814B4h],0 
0000000e  je          00000015 
00000010  call        61993AA3 
00000015  mov         eax,dword ptr [ebp-4] 
00000018  mov         eax,dword ptr [eax+4] 
0000001b  mov         esp,ebp 
0000001d  pop         ebp 
0000001e  ret 

原来今天的编译器对此进行了优化,因此没有性能成本。

output = (TOriginal)(object)oTemp;

这是执行此操作的最佳方式:)。

感谢Eric LippertBen Voigt

关于参考类型的说明:
删除struct约束并传递引用类型(在我的情况下为string)时,此优化是 NOT

结果:

                return (TOut)(object)_value;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,10h 
00000006  mov         dword ptr [ebp-4],edx 
00000009  mov         dword ptr [ebp-10h],ecx 
0000000c  mov         dword ptr [ebp-8],edx 
0000000f  cmp         dword ptr ds:[003314B4h],0 
00000016  je          0000001D 
00000018  call        61A63A43 
0000001d  mov         eax,dword ptr [ebp-8] 
00000020  mov         eax,dword ptr [eax+0Ch] 
00000023  mov         eax,dword ptr [eax] 
00000025  mov         dword ptr [ebp-0Ch],eax 
00000028  test        dword ptr [ebp-0Ch],1 
0000002f  jne         00000036 
00000031  mov         ecx,dword ptr [ebp-0Ch] 
00000034  jmp         0000003C 
00000036  mov         eax,dword ptr [ebp-0Ch] 
00000039  mov         ecx,dword ptr [eax-1] 
0000003c  mov         eax,dword ptr [ebp-10h] 
0000003f  mov         edx,dword ptr [eax+4] 
00000042  call        617D79D8 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

                return _value;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        eax 
00000004  mov         dword ptr [ebp-4],ecx 
00000007  cmp         dword ptr ds:[003314B4h],0 
0000000e  je          00000015 
00000010  call        61A639E3 
00000015  mov         eax,dword ptr [ebp-4] 
00000018  mov         eax,dword ptr [eax+4] 
0000001b  mov         esp,ebp 
0000001d  pop         ebp 
0000001e  ret 

如果你想要一种廉价的方式来投射'没有正确的类型检查',as运算符就是你的解决方案。

3 个答案:

答案 0 :(得分:6)

我打算给你那个你不想要的讲座,因为你显然不理解。

“测量,测量,测量”有两个原因! (或等效地“Profile,Profile,Profile!”)优化方法:

  1. 将努力放在影响最大的地方。这就是“过早优化”一词的用武之地。

    有时这个理由不适用(当你想了解理论/出于学术原因时)。

  2. 找出哪个实施更快。

  3. 现代CPU是复杂的野兽,甚至比较两个不同的机器代码序列也无法显示哪个更好,这是由于缓存行为的复杂性,管道数据依赖性,微代码等等。你正在运行比这高两级(C#代码 - &gt; MSIL - &gt;机器代码)。没有测量就无法确定优化程度。

    你说:

      

    这会绕过编译时错误,但我不想这样做会导致性能下降:

    output = (TOriginal)(object)oTemp;
    

    但我认为那里确实没有任何表现。对于每种值类型,通用方法都是JITted,该过程应该完全消除任何想象的性能损失。但是,欢迎您通过性能数据(实际分析器测量)或至少反汇编JIT生成的机器代码来证明实际存在成本。


    在这种特殊情况下,如果它们始终与您声明的类型相同,则不清楚为什么要开始使用两个不同的泛型类型参数。只需删除TOriginal并使用T作为输出参数的类型。

答案 1 :(得分:4)

我觉得具有讽刺意味的是,本的回答说“使用科学:衡量它来发现”,“这是我对真实情况的看法”:

  

我认为那里确实没有任何表现。对于每种值类型,通用方法都是JITted,该过程应该完全消除任何想象的性能损失。

根据更新后的问题中显示的实际反汇编,原始海报声称使用的抖动显然至少在某些时候进行了优化。我没有分析这个说法是否正确;我想实际看到正在编译的实际代码,IL和生成的程序集,以了解这里发生了什么。

在我过去对这个领域的调查中,我发现了许多情况,其中验证者和抖动都不够聪明,特别是在消除拳击处罚方面。这些是否全部被淘汰我不知道。

如果其中一些已被淘汰,那么我很高兴了解到这一点。

因此,您无法断定表面抖动执行或不执行消除装箱的优化。我见过没有的情况;我们在这里有一个未经证实的声明,在某些情况下确实如此。

Ben继续提出一些好建议:

  

但是,欢迎您通过性能数据(实际分析器测量)或至少反汇编JIT生成的机器代码来证明实际存在成本。

实际上,我强烈建议您这样做,并且不止一次抖动。

让我们重新开始并实际回答所提出的问题。我们应该首先简化和澄清所描述的案例:

abstract class B
{
    public abstract T M<T>() where T : struct; 
}

private class D<U> : B where U : struct
{
    public override V M<V>() 
    {
        U u = default(U);
        return (V)u; // compile-time error
    } 
}

原始海报说V将永远与U相同。这是第一个问题。为什么?没有任何东西阻止用户在M<bool>的实例上调用D<double>。类型检查器完全正确,注意到可能没有从U到V的转换。

正如原始海报所说,你可以通过装箱和拆箱来绕类型检查器进行最终运行:

        return (V)(object)u; // Runtime error, not compile-time error

接下来的问题是“在没有崩溃并且在运行时可怕地死亡的情况下,抖动会消除拳击惩罚吗?”

抖动仅对方法进行一次jits并共享引用类型参数的代码,但每次都为不同的值类型参数重新匹配它。因此,当为U和V提供的特定参数是相同的值类型时,有机会消除惩罚。

我几年前曾想过自己和所以我检查了。我没有检查更多最近的抖动版本,但上次我检查拳击处罚是消除。抖动分配内存,将值复制到堆中,然后再将其复制回来。

显然,根据更新的问题,情况已不再如此;现在测试的抖动执行此优化。就像我说的那样,我自己没有证实这个说法。

允许允许进行优化,但是上次检查时,实际上它没有,所以我们知道在野外至少有一个抖动没有这种优化。

一个更有趣的例子是实际 约束为相等的类型参数:

abstract class E<T>
{
    public abstract U M<U>(T t) where U : T; 
}
class F<V> : E<V> where V : struct
{
    public override W M<W>(V v)
    {
        return v; // Error 
    }
}

同样,这是非法的,即使C#编译器可以逻辑推断W现在必须与V相同。

您可以再次引入强制转换以解决问题,但 IL验证程序的类型分析器要求将V框装箱并取消装箱为W。

再次,抖动可以推断出拳击和拆箱是无操作,并消除它,但我最后一次检查它没有。它现在可能;试一试,看看。

我报告说这是对抖动团队的可能优化;他们告诉我他们有更高的优先级,这是一个非常合理的回应。这是一个模糊且不太可能出现的情况,也不是我要优先考虑的情况。

如果事实上现在已经进行了这种优化,那么我很惊喜。

答案 2 :(得分:0)

您希望避免将T / TOriginal值转换为对象,这会导致出现装箱问题,其中值类型(所有结构都是)将被封装为堆上的System.Object。有几种方法可以解决铸造问题。最简单的方法是让抽象类包含类级泛型类型参数而不是TryGet方法,如:

    public abstract class AbstractParam<T> where T : struct
    {
        //....
        public abstract bool TryGet(string input, out T output);
    }

另一个选项是投放到Nullable<TOriginal>,然后像这样调用GetValueOrDefault()

        public override bool TryGet<TOriginal>(string input, out TOriginal output)
        {
            T oTemp;
            bool res = _func(input, out oTemp);
            Nullable<TOriginal> n = oTemp as Nullable<TOriginal>;
            output = n.GetValueOrDefault();
            return res;
        }