值类型的分配

4

当你将值类型的实例分配给另一个实例时,对象会逐位复制到目标位置:

private struct Word
{
    public Word(char c) { ... }
}

public void Method(Word a)
{
    Word b = a; //a is copied and stored in b
}

但是考虑以下代码:
private Word _word;

public void Method() {
    _word = new Word('x');
}

我怀疑右侧表达式会首先被评估 - 这会在堆栈上实例化一个值类型 - 然后将该值复制并存储在堆上的_word字段位置。
另一种选择是考虑左侧,直接在_word上实例化值类型,避免复制对象。
我的怀疑是否正确?如果是这样,我认为第一个代码块比第二个代码块性能更好。
//1 instantiation + 10k copies
Word[] words = new Word[10000];
Word word = new Word('x');

for (int i = 0; i < 10000; i++)
    words[i] = word;


//10k instantiations + 10k copies
Word[] words = new Word[10000];

for (int i = 0; i < 10000; i++)
    words[i] = new Word('x');

注意:我并不试图微调任何东西。
编辑:我的问题的核心是,正如Lee所说:结构体是直接分配还是需要先分配再复制?

我认为你的意思是最后一行应该是 words[i] = new Word();。这个答案可能很有趣,但我认为如果能够优化它就更好了。 - Weyland Yutani
如果你只是调用默认构造函数,那么循环完全可以被移除,对吧?结构体已经被零初始化了。 - Lee
也许这是未指定的,因为在单线程执行下无法区分它们的差异。当然,如果构造函数执行副作用,那么你的两个代码片段将不等价。 - usr
@dcastro。看来我错了。但是那个人亲自回答了你 :) - Murdock
什么是使用案例?你真的需要制作多个相同结构体的副本吗? - paparazzo
显示剩余7条评论
2个回答

9
当你将值类型的实例分配给另一个实例时,对象会逐位复制到目标位置。
当你将值类型的实例分配给相同类型的变量时,值会被复制到目标位置。但是对于引用类型也是如此:引用会逐位复制到目标位置,而引用指向的对象则保持不变。
我怀疑右侧表达式会首先被评估。
规范说明左侧被评估以产生一个变量,然后右侧被评估以产生一个值,然后进行赋值。
在你给出的示例中,左侧的评估没有产生可观察的副作用,因此如果C#编译器、JIT或CPU选择这样做,优化器可以重新排序它的评估。但是如果你有一些像下面这样的东西:
x[i++] = v();

如果左侧的副作用必须在右侧的函数调用之前发生。

我的问题的核心是:结构体是直接在原地分配还是需要先分配再复制?

规范说明结构体是在临时位置(通常是栈或寄存器)中分配,然后复制到它们的最终目的地。但是,在某些情况下,优化器可以确定如果在最终目的地处“原地”进行变异,则用户不可能注意到。这是一种复制省略优化,如果C#编译器认为可以逃避,则会执行此优化。

有关详细信息,请参阅我关于此主题的文章:

http://ericlippert.com/2010/10/11/debunking-another-myth-about-value-types/


感谢您的精彩解释,特别是那篇博客文章,解释了为什么结构体通常会被分配到临时位置。因此,我认为从我提供的两个代码示例中,第一个示例更好,因为它可以避免反复在临时位置分配结构体(假设没有复制省略),这正确吗? - dcastro
哦,当我说“先评估RHS表达式”时,我想说的是:“RHS和LHS是独立评估的”。也就是说,LHS不会影响结构体的分配,但现在我知道这并不一定是真的。 - dcastro
@dcastro:C#编译器仅在目标为局部变量时省略副本;在您的示例中,目标是数组元素。您第一个和第二个示例之间的真正节省可能不在复制上,而是避免调用9999次构造函数。 - Eric Lippert

0

哪个更受欢迎?

多个相同结构体的业务案例是什么?
如果需要多个相同对象,结构体是否是最佳选择?

以这种方式重新初始化的结构体可能不是示例用例的好解决方案
在新数组中,WordStruct使用默认构造函数(无构造函数)进行分配和初始化
您无法使用另一个构造函数初始化结构体数组
如果确实需要相同的结构体,则应首选此选项

WordStruct[] WordStructS = new WordStruct[1000];
for (int i = 0; i < WordStructS.Length; i++) { WordStructS[i].C = 'x'; }

如果有多个相同的对象,考虑使用类来处理
类会分配一个新的数组,但尚未初始化
不需要使用默认构造函数来初始化,这样会浪费资源。
WordClass[] WordClassS = new WordClass[1000];
for (int i = 0; i < WordClassS.Length; i++) { WordClassS[i] = new WordClass('x'); }

如果您想要泛化一个对象(结构体或类)的深拷贝,那么请考虑使用 IConable 接口。
对于结构体而言,我认为它比位拷贝更高效(但我不确定)。
对于类而言,它将创建一个克隆(深拷贝),而非引用。
public struct WordStruct : ICloneable 
{
    public char C;
    public WordStruct(char C) 
    {
        this.C = C;
    }
    public object Clone()
    {
        WordStruct newWordStruct = (WordStruct)this.MemberwiseClone();
        return newWordStruct;
    }
}

我知道你在评论中提到了好奇心,但问题并不清楚。
在问题中,你说第一段代码比第二段代码更受欢迎。

我明白这是一个有趣的好奇问题
但如果只是好奇,那么问题应该停留在
我的怀疑是否正确?


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