为什么结构对齐取决于字段类型是原始类型还是用户定义的? [英] Why does struct alignment depend on whether a field type is primitive or user-defined?

查看:22
本文介绍了为什么结构对齐取决于字段类型是原始类型还是用户定义的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Noda Time v2 中,我们正在转向纳秒分辨率.这意味着我们不能再使用一个 8 字节的整数来表示我们感兴趣的整个时间范围.这促使我调查 Noda Time 的(许多)结构的内存使用情况,这反过来又引导我发现 CLR 对齐决策中的一个小奇怪之处.

In Noda Time v2, we're moving to nanosecond resolution. That means we can no longer use an 8-byte integer to represent the whole range of time we're interested in. That has prompted me to investigate the memory usage of the (many) structs of Noda Time, which has in turn led me to uncover a slight oddity in the CLR's alignment decision.

首先,我意识到这是一个实现决策,并且默认行为可能随时更改.我意识到我可以使用修改它[StructLayout][FieldOffset],但如果可能的话,我宁愿提出一个不需要的解决方案.

Firstly, I realize that this is an implementation decision, and that the default behaviour could change at any time. I realize that I can modify it using [StructLayout] and [FieldOffset], but I'd rather come up with a solution which didn't require that if possible.

我的核心场景是我有一个 struct,它包含一个引用类型字段和两个其他值类型字段,其中这些字段是 int 的简单包装器.我曾希望在 64 位 CLR 上将其表示为 16 个字节(8 个用于参考,每个其他 4 个),但出于某种原因,它使用了 24 个字节.顺便说一下,我正在使用数组来测量空间 - 我知道布局在不同情况下可能会有所不同,但这感觉是一个合理的起点.

My core scenario is that I have a struct which contains a reference-type field and two other value-type fields, where those fields are simple wrappers for int. I had hoped that that would be represented as 16 bytes on the 64-bit CLR (8 for the reference and 4 for each of the others), but for some reason it's using 24 bytes. I'm measuring the space using arrays, by the way - I understand that the layout may be different in different situations, but this felt like a reasonable starting point.

以下是演示该问题的示例程序:

Here's a sample program demonstrating the issue:

using System;
using System.Runtime.InteropServices;

#pragma warning disable 0169

struct Int32Wrapper
{
    int x;
}

struct TwoInt32s
{
    int x, y;
}

struct TwoInt32Wrappers
{
    Int32Wrapper x, y;
}

struct RefAndTwoInt32s
{
    string text;
    int x, y;
}

struct RefAndTwoInt32Wrappers
{
    string text;
    Int32Wrapper x, y;
}    

class Test
{
    static void Main()
    {
        Console.WriteLine("Environment: CLR {0} on {1} ({2})",
            Environment.Version,
            Environment.OSVersion,
            Environment.Is64BitProcess ? "64 bit" : "32 bit");
        ShowSize<Int32Wrapper>();
        ShowSize<TwoInt32s>();
        ShowSize<TwoInt32Wrappers>();
        ShowSize<RefAndTwoInt32s>();
        ShowSize<RefAndTwoInt32Wrappers>();
    }

    static void ShowSize<T>()
    {
        long before = GC.GetTotalMemory(true);
        T[] array = new T[100000];
        long after  = GC.GetTotalMemory(true);        
        Console.WriteLine("{0}: {1}", typeof(T),
                          (after - before) / array.Length);
    }
}

以及我笔记本电脑上的编译和输出:

And the compilation and output on my laptop:

c:UsersJonTest>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.


c:UsersJonTest>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24

所以:

  • 如果您没有引用类型字段,CLR 很乐意将 Int32Wrapper 字段打包在一起(TwoInt32Wrappers 的大小为 8)
  • 即使使用引用类型字段,CLR 仍然乐于将 int 字段打包在一起(RefAndTwoInt32s 的大小为 16)
  • 将两者结合起来,每个 Int32Wrapper 字段似乎都填充/对齐到 8 个字节.(RefAndTwoInt32Wrappers 的大小为 24.)
  • 在调试器中运行相同的代码(但仍然是发布版本)显示大小为 12.
  • If you don't have a reference type field, the CLR is happy to pack Int32Wrapper fields together (TwoInt32Wrappers has a size of 8)
  • Even with a reference type field, the CLR is still happy to pack int fields together (RefAndTwoInt32s has a size of 16)
  • Combining the two, each Int32Wrapper field appears to be padded/aligned to 8 bytes. (RefAndTwoInt32Wrappers has a size of 24.)
  • Running the same code in the debugger (but still a release build) shows a size of 12.

其他一些实验也得出了类似的结果:

A few other experiments have yielded similar results:

  • 将引用类型字段放在值类型字段之后没有帮助
  • 使用 object 而不是 string 没有帮助(我希望它是任何引用类型")
  • 使用另一个结构体作为引用的包装器"无济于事
  • 使用通用结构作为引用的包装器无济于事
  • 如果我继续添加字段(为简单起见,成对添加),int 字段仍占 4 个字节,而 Int32Wrapper 字段占 8 个字节
  • [StructLayout(LayoutKind.Sequential, Pack = 4)] 添加到每个结构中不会改变结果
  • Putting the reference type field after the value type fields doesn't help
  • Using object instead of string doesn't help (I expect it's "any reference type")
  • Using another struct as a "wrapper" around the reference doesn't help
  • Using a generic struct as a wrapper around the reference doesn't help
  • If I keep adding fields (in pairs for simplicity), int fields still count for 4 bytes, and Int32Wrapper fields count for 8 bytes
  • Adding [StructLayout(LayoutKind.Sequential, Pack = 4)] to every struct in sight doesn't change the results

有没有人对此有任何解释(最好有参考文档)或建议我如何向 CLR 提示我希望在不指定常量字段的情况下打包字段抵消?

Does anyone have any explanation for this (ideally with reference documentation) or a suggestion of how I can get hint to the CLR that I'd like the fields to be packed without specifying a constant field offset?

推荐答案

我认为这是一个错误.您正在看到自动布局的副作用,它喜欢将非平凡字段与 64 位模式下 8 字节倍数的地址对齐.即使您显式应用 [StructLayout(LayoutKind.Sequential)] 属性,它也会发生.这不应该发生.

I think this is a bug. You are seeing the side-effect of automatic layout, it likes to align non-trivial fields to an address that's a multiple of 8 bytes in 64-bit mode. It occurs even when you explicitly apply the [StructLayout(LayoutKind.Sequential)] attribute. That is not supposed to happen.

您可以通过将结构成员公开并附加如下测试代码来查看它:

You can see it by making the struct members public and appending test code like this:

    var test = new RefAndTwoInt32Wrappers();
    test.text = "adsf";
    test.x.x = 0x11111111;
    test.y.x = 0x22222222;
    Console.ReadLine();      // <=== Breakpoint here

当断点命中时,使用 Debug + Windows + Memory + Memory 1. 切换到 4 字节整数并将 &test 放在地址字段中:

When the breakpoint hits, use Debug + Windows + Memory + Memory 1. Switch to 4-byte integers and put &test in the Address field:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0 是我机器上的字符串指针(不是你的).您可以很容易地看到 Int32Wrappers,额外的 4 字节填充将大小变为 24 字节.返回结构并将字符串放在最后.重复,您将看到字符串指针 仍然 首先.违反 LayoutKind.Sequential,你得到了 LayoutKind.Auto.

0xe90ed750e0 is the string pointer on my machine (not yours). You can easily see the Int32Wrappers, with the extra 4 bytes of padding that turned the size into 24 bytes. Go back to the struct and put the string last. Repeat and you'll see the string pointer is still first. Violating LayoutKind.Sequential, you got LayoutKind.Auto.

要说服 Microsoft 解决这个问题会很困难,它已经以这种方式工作了太久,因此任何更改都会破坏某些事情.CLR 只是尝试为结构的托管版本提供 [StructLayout] 并使其成为 blittable,它通常很快就会放弃.众所周知,任何包含 DateTime 的结构体.在编组结构时,您只能获得真正的 LayoutKind 保证.正如 Marshal.SizeOf() 会告诉你的,编组版本肯定是 16 个字节.

It is going to be difficult to convince Microsoft to fix this, it has worked this way for too long so any change is going to be breaking something. The CLR only makes an attempt to honor [StructLayout] for the managed version of a struct and make it blittable, it in general quickly gives up. Notoriously for any struct that contains a DateTime. You only get the true LayoutKind guarantee when marshaling a struct. The marshaled version certainly is 16 bytes, as Marshal.SizeOf() will tell you.

使用 LayoutKind.Explicit 修复它,而不是你想听到的.

Using LayoutKind.Explicit fixes it, not what you wanted to hear.

这篇关于为什么结构对齐取决于字段类型是原始类型还是用户定义的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆