.NET装箱/拆箱 vs 强制类型转换性能

11
我试图了解在性能方面哪种解决方案更受欢迎。例如,我有两段代码:

1) 装箱 / 拆箱

int val = 5;
Session["key"] = val; 
int val2 = (int)Session["key"];

2) 强制类型转换(IntObj 有 int Value 属性来存储整数)

IntObj val = new IntObj(5);  
Session["key"] = val;
int val2 = ((IntObj )Session["key"]).Value;
这些示例之间的内存管理差异是什么? 有没有更快的方法执行这样的操作?
注意:`Session`仅用作示例,它可以是任何`Dictionary`。
答案:这些示例之间的内存管理差异是指第一个示例使用了扩展方法来进行操作,而第二个示例则手动迭代字典。在处理大型字典时,使用扩展方法可以更高效地使用内存。至于第二个问题,可能需要进一步的上下文信息以回答。请注意,`Session`只是一个示例,它可以是任何`Dictionary`。

2
我没有测量过,但我认为基元会更快。对象有开销。但在这种情况下,我认为可读性是一个更重要的指标。CPU周期比脑力消耗便宜得多。 - Sjuul Janssen
4个回答

9
看起来你在比较手动装箱和内置装箱。内置装箱已经高度优化,因此我不希望在这里看到很大的差异,但我们可以检查一下。重要的是要注意两者具有相同的内存影响:一个堆对象包含一个 int 字段,每个 int 装箱/封装。

以下显示了两种方法的几乎相同时间;因此,我建议直接或通过内置方式装箱。
注意:在释放模式下运行它,不要使用调试器(最好在命令行中)。请注意,第一个调用存在于预JIT状态。
using System;
using System.Diagnostics;
public sealed class IntObj
{
    public readonly int Value;
    public IntObj(int value)
    {
        Value = value;
    }
}
static class Program
{
    static void Main()
    {
        Run(1, 0, false);
        Run(100000, 500, true);
        Console.ReadKey();
    }
    static void Run(int length, int repeat, bool report)
    {
        var data = new object[length];

        int chk = 0;
        var watch = Stopwatch.StartNew();
        for (int j = 0; j < repeat; j++)
        {
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = i;
                chk += i;
            }
        }
        watch.Stop();
        if(report) Console.WriteLine("Box: {0}ms (chk: {1})", watch.ElapsedMilliseconds, chk);
        chk = 0;
        watch = Stopwatch.StartNew();
        for (int j = 0; j < repeat; j++)
        {
            for (int i = 0; i < data.Length; i++)
            {
                chk += (int) data[i];
            }
        }
        watch.Stop();
        if (report) Console.WriteLine("Unbox: {0}ms (chk: {1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int j = 0; j < repeat; j++)
        {
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = new IntObj(i);
                chk += i;
            }
        }
        watch.Stop();
        if (report) Console.WriteLine("Wrap: {0}ms (chk: {1})", watch.ElapsedMilliseconds, chk);
        chk = 0;
        watch = Stopwatch.StartNew();
        for (int j = 0; j < repeat; j++)
        {
            for (int i = 0; i < data.Length; i++)
            {
                chk += ((IntObj)data[i]).Value;
            }
        }
        watch.Stop();
        if (report) Console.WriteLine("Unwrap: {0}ms (chk: {1})", watch.ElapsedMilliseconds, chk);
    }


}

谢谢!现在很容易看到了 :) - Alex Dn

4

那么,使用 IntObj 进行自制拳击比使用内置的 boxing 更快吗?

我的猜测是内置的 boxing 更快。很可能两种编译器都经过了优化以处理它。

有没有更 "快速" 的方法来执行这样的操作?

最好的方法总是避免在大型数据集上使用它。对于小数据集,它根本没有意义。


我举了一个例子的会话,它可能是一个简单的Dictionary<string, object>。 - Alex Dn
我假设问题是关于IntObj的。容器(Session)并不是非常相关。 - H H

2

最快的方法不是去做某件事情,而是避免大量的装箱操作来增加类型安全性、可读性和潜在的性能提升。

我认为你很少需要在未知类型的字典中存储大量无关的整数(或其他值类型)元素。通常情况下,值会被组织成一些有意义的对象,这种情况下,你只需将顶层对象存储在未知类型的字典中,并且只需要一个强制转换。对于更深层的元素,你应该使用强类型类(例如Dictionary<string,int>),因为这个问题已经得到了解决,不需要进行任何装箱操作。

如果你觉得在你的情况下确实需要在字符串=>对象映射中存储大量int(或其他值类型元素),那么使用你的数据集和目标进行自己的测量非常容易,以查看哪个版本具有显着的优势。如果两个版本都满足你的目标(很可能是这样),那么选择生成最可读代码的版本(例如,对我来说,第一个版本就是最可读的版本)。


0

我将由C#强制转换运算符生成的不同类型的IL指令进行了分类:

装箱(box IL指令)和拆箱(unbox IL指令) 通过继承层次结构进行转换(类似于C++中的dynamic_cast,使用castclass IL指令进行验证) 在基本类型之间进行转换(类似于C++中的static_cast,有许多不同类型的IL指令用于基本类型之间的转换) 调用用户定义的转换运算符(在IL级别上,它们只是对适当的op_XXX方法的方法调用)。

区别在于强制转换会分配额外的内存,因为新的引用类型被创建。


4
那么这怎么回答问题呢?(拳击还会在堆上创建一个新实例) - H H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接