<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      理解 .NET 結(jié)構(gòu)體字段的內(nèi)存布局

      前言

      大部分情況下我們并不需要關(guān)心結(jié)構(gòu)體字段的內(nèi)存布局,但是在一些特殊情況下,比如性能優(yōu)化、和非托管代碼交互、對結(jié)構(gòu)體進(jìn)行序列化等場景下,了解字段的內(nèi)存布局是非常重要的。

      本文寫作時(shí) 最新的 .NET 正式版是 .NET 9,以后的版本不保證本文內(nèi)容的準(zhǔn)確性,僅供參考。

      本文將介紹 .NET 中結(jié)構(gòu)體字段的內(nèi)存布局,包括字段的對齊(Alignment)、填充(Padding)以及如何使用 StructLayoutAttribute 來控制字段的內(nèi)存布局。

      對齊的目的是為了 CPU 訪問內(nèi)存的效率,64 位系統(tǒng)和 32 位系統(tǒng)中對齊要求存在差異,下文如果沒有特別說明,均指 64 位系統(tǒng)。

      填充則是為了滿足對齊要求而在字段之間或結(jié)構(gòu)體末尾添加的額外字節(jié)。

      結(jié)構(gòu)體的對其規(guī)則同時(shí)適用于棧上和堆上的結(jié)構(gòu)體結(jié)構(gòu)體實(shí)例,方便起見,大部分例子將使用棧上結(jié)構(gòu)體實(shí)例來演示。

      一些資料是從 字段的偏移量(offset)為出發(fā)點(diǎn)來介紹字段的內(nèi)存布局的,但筆者認(rèn)為從字段的 內(nèi)存地址 出發(fā)更容易理解。

      由于一些資料并沒有找到明確的官方的解釋,筆者是在實(shí)驗(yàn)和推導(dǎo)的基礎(chǔ)上總結(jié)出這些規(guī)則的,可能會有不準(zhǔn)確的地方,歡迎讀者在評論區(qū)指出。

      本文雖然沒有直接介紹引用類型的字段布局,但引用類型實(shí)例的字段的內(nèi)存布局概念與結(jié)構(gòu)體實(shí)例的內(nèi)存布局是相同的。不同之處在于引用類型的默認(rèn)布局是 LayoutKind.Auto,而結(jié)構(gòu)體的默認(rèn)布局是 LayoutKind.Sequential。讀者可以自己嘗試觀察引用類型實(shí)例字段的內(nèi)存布局。

      本文將使用下面的方法來觀察字段的內(nèi)存地址:

      // 打印日志頭
      void PrintPointerHeader()
      {
          Console.WriteLine(
              $"| {"Expr",-15} | {"Address",-15} | {"Size",-4} | {"AlignedBySize",-13} | {"Addr/Size",-12} |");
      }
      
      // 打印指針的詳細(xì)信息
      unsafe void PrintPointerDetails<T>(
          T* ptr,
          [CallerArgumentExpression("ptr")] string? pointerExpr = null)
          where T : unmanaged
      {
          ulong addressValue = (ulong)ptr;
          ulong typeSize = (ulong)sizeof(T);
      
          decimal addressDivBySize = addressValue / (decimal)typeSize;
          bool isAlignedBySize = addressValue % typeSize == 0;
      
          Console.WriteLine(
              $"| {pointerExpr,-15} | {addressValue,-15} | {typeSize,-4} | {isAlignedBySize,-13} | {addressDivBySize,-12:0.##} |"
          );
      }
      

      并使用 ObjectLayoutInspector 這個(gè)開源庫來觀察字段的內(nèi)存布局。

      項(xiàng)目地址:https://github.com/SergeyTeplyakov/ObjectLayoutInspector

      nuget 包地址:https://www.nuget.org/packages/ObjectLayoutInspector

      dotnet add package ObjectLayoutInspector --version 0.1.4
      

      基本概念

      以下是理解結(jié)構(gòu)體字段布局的幾個(gè)關(guān)鍵點(diǎn):

      • 字段順序:字段在結(jié)構(gòu)體實(shí)例中的排列順序,默認(rèn)按聲明順序排列,但可以通過 StructLayoutAttribute 來控制。

      • 對齊(Alignment):對齊需要分成三部分理解:

        • 字段的對齊要求(alignment requirement):指字段在內(nèi)存中的地址必須是其對齊要求的倍數(shù)。對于基元類型(primitive types),對齊要求默認(rèn)等于其大小,非基元類型的對齊要求取決于結(jié)構(gòu)體中最大字段的對齊要求。
        • 結(jié)構(gòu)體實(shí)例的大小:必須是結(jié)構(gòu)體對齊要求的整數(shù)倍。
        • 結(jié)構(gòu)體實(shí)例的起始地址:在 64 位系統(tǒng)中,數(shù)據(jù)的地址按 8 字節(jié) 對齊有利于提升 CPU 的訪問效率,32 位系統(tǒng)中則為 4 字節(jié)對齊。
      • 填充(Padding):為了滿足對齊要求,runtime 可能會在結(jié)構(gòu)體實(shí)例字段之間及末尾插入填充字節(jié)。這些填充字節(jié)不會被顯式聲明,但會影響字段在內(nèi)存中的實(shí)際布局。

      結(jié)構(gòu)體的默認(rèn)字段布局

      對齊

      字段默認(rèn)的對齊要求是類型的大小。例如,int 類型的字段需要在 4 字節(jié)對齊邊界(alignment boundary)上,而 double 類型的字段需要在 8 字節(jié)對齊邊界上。如果字段類型并非基元類型(primitive types),則對齊要求取決于結(jié)構(gòu)體中最大字段的對齊要求。對齊要求為 2 的整數(shù)次冪,例如 1、2、4、8 等。最大對齊要求為 8 字節(jié)。

      注意:decimal 不屬于基元類型,目前版本中由三個(gè)字段組成,實(shí)例大小為 16 字節(jié),按 8 字節(jié)對齊。

      Type layout for 'Decimal'
      Size: 16 bytes. Paddings: 0 bytes (%0 of empty space)
      |===============================|
      |   0-3: Int32 _flags (4 bytes) |
      |-------------------------------|
      |   4-7: UInt32 _hi32 (4 bytes) |
      |-------------------------------|
      |  8-15: UInt64 _lo64 (8 bytes) |
      |===============================|
      

      下面是一個(gè)簡單的示例,展示了結(jié)構(gòu)體字段的默認(rèn)布局:

      using System.Runtime.CompilerServices;
      
      var foo = new Foo();
      var bar = new Bar();
      var baz = new Baz();
      
      unsafe
      {
          PrintPointerHeader();
      
          PrintPointerDetails(&foo);
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
      
          PrintPointerDetails(&bar);
          PrintPointerDetails(&bar.foo);
          PrintPointerDetails(&bar.foo.a);
          PrintPointerDetails(&bar.foo.b);
      
          fixed (Foo* bazFooPtr = &baz.foo)
          {
              PrintPointerDetails(bazFooPtr);
              PrintPointerDetails(&bazFooPtr->a);
              PrintPointerDetails(&bazFooPtr->b);
          }
      }
      
      struct Foo
      {
          public int a;
          public long b;
      }
      
      struct Bar
      {
          public Foo foo;
      }
      
      class Baz
      {
          public Foo foo;
      }
      

      輸出結(jié)果如下:

      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo            | 6095528264      | 16   | False         | 380970516.5  |
      | &foo.a          | 6095528264      | 4    | True          | 1523882066   |
      | &foo.b          | 6095528272      | 8    | True          | 761941034    |
      | &bar            | 6095528248      | 16   | False         | 380970515.5  |
      | &bar.foo        | 6095528248      | 16   | False         | 380970515.5  |
      | &bar.foo.a      | 6095528248      | 4    | True          | 1523882062   |
      | &bar.foo.b      | 6095528256      | 8    | True          | 761941032    |
      | bazFooPtr       | 12885617264     | 16   | True          | 805351079    |
      | &bazFooPtr->a   | 12885617264     | 4    | True          | 3221404316   |
      | &bazFooPtr->b   | 12885617272     | 8    | True          | 1610702159   |
      

      首先看 Foo 結(jié)構(gòu)體,它有兩個(gè)字段 ab,分別是 intlong 類型,對齊要求分別是 4 字節(jié)和 8 字節(jié)。

      所以 Foo 實(shí)例在棧上的地址按照 8 字節(jié) 對齊(6095528264 / 8 = 761941033)。

      a 字段是 foo 的第一個(gè)字段,它的地址也就是 foo 的起始地址,自然也滿足 int 的對齊要求(6095528264 / 4 = 1523882066)。

      b 字段是 foo 的第二個(gè)字段,它的地址為 6095528272,滿足 long 的對齊要求(6095528272 / 8 = 761941032)。

      Bar 結(jié)構(gòu)體包含一個(gè) Foo 類型的字段 foo,它的對齊要求也是 8 字節(jié)(取最大字段 long 的對齊要求),所以 bar 的地址也是按照 8 字節(jié)對齊(6095528248 / 8 = 761941031)。bar.foo.abar.foo.b 的地址也滿足各自的對齊要求。

      Baz 類包含一個(gè) Foo 類型的字段 foo,由于 Baz 是引用類型,所以它的實(shí)例在堆上分配內(nèi)存。BazFoo 類型字段也依舊需要滿足 8 字節(jié)對齊要求(12885617264 / 8 = 1610702158)。

      64 位系統(tǒng)與 32 位系統(tǒng)的對齊要求差異

      在 64 位系統(tǒng)中,結(jié)構(gòu)體實(shí)例的起始地址默認(rèn)按 8 字節(jié)對齊。

      而在 32 位系統(tǒng)中,結(jié)構(gòu)體實(shí)例的起始地址默認(rèn)按 4 字節(jié)對齊。經(jīng)筆者測試,CPU 為 intel 時(shí) 只按 4 字節(jié)對齊,CPU 為 AMD 時(shí) 如果結(jié)構(gòu)體包含了 8 字節(jié)對齊的字段,則按 8 字節(jié)對齊,否則按 4 字節(jié)對齊。

      首先在 64 位系統(tǒng)上運(yùn)行下面的代碼:

      using System.Runtime.CompilerServices;
      using ObjectLayoutInspector;
      
      unsafe
      {
          var foo = new Foo();
          var bar = new Bar();
      
          // 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已經(jīng)定義
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
          PrintPointerDetails(&bar.d);
          PrintPointerDetails(&bar.e);
          PrintPointerDetails(&bar.f);
      }
      
      TypeLayout.PrintLayout<Foo>();
      TypeLayout.PrintLayout<Bar>();
      
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      struct Bar
      {
          public int d;
          public int e;
          public byte f;
      }
      

      輸出結(jié)果如下:

      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 985964996520    | 4    | True          | 246491249130 |
      | &foo.b          | 985964996528    | 8    | True          | 123245624566 |
      | &foo.c          | 985964996536    | 1    | True          | 985964996536 |
      | &bar.d          | 985964996504    | 4    | True          | 246491249126 |
      | &bar.e          | 985964996508    | 4    | True          | 246491249127 |
      | &bar.f          | 985964996512    | 1    | True          | 985964996512 |
      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |   4-7: padding (4 bytes) |
      |--------------------------|
      |  8-15: Int64 b (8 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      
      
      Type layout for 'Bar'
      Size: 12 bytes. Paddings: 3 bytes (%25 of empty space)
      |==========================|
      |   0-3: Int32 d (4 bytes) |
      |--------------------------|
      |   4-7: Int32 e (4 bytes) |
      |--------------------------|
      |     8: Byte f (1 byte)   |
      |--------------------------|
      |  9-11: padding (3 bytes) |
      |==========================|
      

      可以看到,FooBar 結(jié)構(gòu)體的實(shí)例大小分別為 24 字節(jié)和 12 字節(jié),且它們的起始地址都滿足 8 字節(jié)對齊要求。

      在 Windows 環(huán)境中,如果安裝了 x86 版本的 .NET SDK,可以在 csproj 文件中添加以下屬性來讓項(xiàng)目運(yùn)行在 32 位的環(huán)境中:

      <PropertyGroup>
          <RuntimeIdentifier>win-x86</RuntimeIdentifier>
      </PropertyGroup>
      

      下面是 intel CPU 的輸出結(jié)果:

      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 43511772        | 4    | True          | 10877943     |
      | &foo.b          | 43511780        | 8    | False         | 5438972.5    |
      | &foo.c          | 43511788        | 1    | True          | 43511788     |
      | &bar.d          | 43511760        | 4    | True          | 10877940     |
      | &bar.e          | 43511764        | 4    | True          | 10877941     |
      | &bar.f          | 43511768        | 1    | True          | 43511768     |
      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |   4-7: padding (4 bytes) |
      |--------------------------|
      |  8-15: Int64 b (8 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      
      
      Type layout for 'Bar'
      Size: 12 bytes. Paddings: 3 bytes (%25 of empty space)
      |==========================|
      |   0-3: Int32 d (4 bytes) |
      |--------------------------|
      |   4-7: Int32 e (4 bytes) |
      |--------------------------|
      |     8: Byte f (1 byte)   |
      |--------------------------|
      |  9-11: padding (3 bytes) |
      |==========================|
      

      FooBar 結(jié)構(gòu)體的起始地址和字段地址都只滿足 4 字節(jié)對齊要求(43511772 / 4 = 10877943),而不是 8 字節(jié)對齊要求。

      下面是 AMD CPU 的輸出結(jié)果:

      
      運(yùn)行上述代碼,輸出結(jié)果如下:
      
      ```bash
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 47706560        | 4    | True          | 11926640     |
      | &foo.b          | 47706568        | 8    | True          | 5963321      |
      | &foo.c          | 47706576        | 1    | True          | 47706576     |
      | &bar.d          | 47706548        | 4    | True          | 11926637     |
      | &bar.e          | 47706552        | 4    | True          | 11926638     |
      | &bar.f          | 47706556        | 1    | True          | 47706556     |
      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |   4-7: padding (4 bytes) |
      |--------------------------|
      |  8-15: Int64 b (8 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      
      
      Type layout for 'Bar'
      Size: 12 bytes. Paddings: 3 bytes (%25 of empty space)
      |==========================|
      |   0-3: Int32 d (4 bytes) |
      |--------------------------|
      |   4-7: Int32 e (4 bytes) |
      |--------------------------|
      |     8: Byte f (1 byte)   |
      |--------------------------|
      |  9-11: padding (3 bytes) |
      |==========================|
      

      Foo 的起始地址仍然滿足 8 字節(jié)對齊要求,但 Bar 的起始地址不再滿足 8 字節(jié)對齊要求(47706548 / 8 = 5963318.5),而是滿足 4 字節(jié)對齊要求(47706548 / 4 = 11926637)。

      默認(rèn)字段布局中 對齊要求 與 偏移量 的關(guān)系

      偏移量(offset)是指字段相對于結(jié)構(gòu)體實(shí)例起始地址的距離,決定了字段在內(nèi)存中的位置。

      偏移量的值取決于對齊要求和字段的順序,會在未被順序在前的字段占用的內(nèi)存空間中取對齊要求的最小整數(shù)倍。

      下面幾個(gè)設(shè)計(jì)確保了不管結(jié)構(gòu)體實(shí)例的起始地址如何,任意一個(gè)字段只要給定一個(gè)滿足對齊要求的偏移量,就可以滿足該字段的對齊要求:

      • 對齊要求總是 2 的整數(shù)次冪。
      • 實(shí)例的起始地址(按 8 字節(jié) 對齊)總是滿足最大字段的對齊要求。
      • 偏移量的值是對齊要求的整數(shù)倍

      下面做一個(gè)簡單的推導(dǎo)來幫助讀者理解:

      假設(shè)結(jié)構(gòu)體中最大字段的對齊要求為 2^m(m 為 <= 8 的非負(fù)整數(shù)),則 runtime 會保證結(jié)構(gòu)體實(shí)例的起始地址也是 2^m 的整數(shù)倍,可記作 2^m * k(k為非負(fù)整數(shù))。

      若某字段的對齊要求為 2^n(n≤m),其偏移量必為 2^n 的整數(shù)倍,記為 2^n * f(f為非負(fù)整數(shù))。

      則該字段實(shí)際地址為:

      結(jié)構(gòu)體起始地址 + 字段偏移量 = (2^m * k) + (2^n * f)

      由于 2^m 必定可以被 2^n 整除(因?yàn)?n≤m),所以無論 k 和 f 取何值,上述字段地址總能被 2^n 整除。這就保證了該字段的地址總是滿足其對齊要求。

      因此,只要給每個(gè)字段的 偏移量 選擇其 對齊要求 的整數(shù)倍,就能保證結(jié)構(gòu)體任何實(shí)例、任意字段的地址都天然對齊,而無需依賴結(jié)構(gòu)體起始地址的額外信息。

      unsafe
      {
          var foo = new Foo();
      
          var addr = (ulong)&foo;
      
          Console.WriteLine($"a offset: {(ulong)&foo.a - addr}");
          Console.WriteLine($"b offset: {(ulong)&foo.b - addr}");
          Console.WriteLine($"c offset: {(ulong)&foo.c - addr}");
      }
      
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      

      輸出結(jié)果如下:

      a offset: 0
      b offset: 8
      c offset: 16
      

      填充

      填充(Padding)分為兩部分:

      1. 字段之間的填充:為了滿足對齊要求,.NET 可能會在字段之間插入填充字節(jié)。字段之間的填充由字段的偏移量決定。

      2. 結(jié)構(gòu)體末尾的填充:為了確保結(jié)構(gòu)體的大小是最大字段對齊要求的倍數(shù),.NET 可能會在結(jié)構(gòu)體末尾添加填充字節(jié)。末尾填充保證了數(shù)組中連續(xù)的結(jié)構(gòu)體實(shí)例在內(nèi)存中也滿足對齊要求。

      借助 ObjectLayoutInspector 庫,我們可以觀察到結(jié)構(gòu)體的內(nèi)存布局,包括字段之間的填充和結(jié)構(gòu)體末尾的填充。

      using ObjectLayoutInspector;
      
      TypeLayout.PrintLayout<Foo>();
      TypeLayout.PrintLayout<Bar>();
      
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      struct Bar
      {
          public byte c;
          public int a;
          public long b;
      }
      

      輸出結(jié)果如下:

      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |   4-7: padding (4 bytes) |
      |--------------------------|
      |  8-15: Int64 b (8 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      
      
      Type layout for 'Bar'
      Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
      |==========================|
      |     0: Byte c (1 byte)   |
      |--------------------------|
      |   1-3: padding (3 bytes) |
      |--------------------------|
      |   4-7: Int32 a (4 bytes) |
      |--------------------------|
      |  8-15: Int64 b (8 bytes) |
      |==========================|
      

      FooBar 雖然包含了相同類型的字段,但由于字段的順序不同,導(dǎo)致它們的內(nèi)存布局和填充字節(jié)數(shù)量也不同。Foo 需要在在末尾添加 7 字節(jié)的填充才能滿足其大小是最大字段對齊要求的倍數(shù)。

      包含引用類型字段的結(jié)構(gòu)體的默認(rèn)字段布局

      如果結(jié)構(gòu)體包含引用類型字段,則該結(jié)構(gòu)體的默認(rèn)布局為 LayoutKind.Auto

      using ObjectLayoutInspector;
      
      TypeLayout.PrintLayout<Foo>();
      
      struct Foo
      {
          public int a;
          public string b;
          public byte c;
      }
      
      Type layout for 'Foo'
      Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
      |===========================|
      |   0-7: String b (8 bytes) |
      |---------------------------|
      |  8-11: Int32 a (4 bytes)  |
      |---------------------------|
      |    12: Byte c (1 byte)    |
      |---------------------------|
      | 13-15: padding (3 bytes)  |
      |===========================|
      

      StructLayoutAttribute 控制字段布局

      在某些情況下,我們可能需要控制結(jié)構(gòu)體字段的內(nèi)存布局,以滿足特定的性能要求或與非托管代碼交互。可以使用 StructLayoutAttribute 特性來控制結(jié)構(gòu)體的內(nèi)存布局。

      StructLayoutAttribute 有兩個(gè)重要的屬性:

      • LayoutKind:指定結(jié)構(gòu)體的布局方式,可以是 Sequential(按聲明順序排列)、Explicit(顯式指定字段偏移量)或 Auto(自動布局)。

      • Pack:指定結(jié)構(gòu)體及其字段的對齊要求,其值必須為 0、1、2、4、8、16、32、64 或 128,否則無法編譯成功,默認(rèn)值為 0。** 指定 Pack > 8 時(shí), 等效于 Pack = 8,因?yàn)槟壳鞍姹緵]有任何類型的對齊要求超過 8 字節(jié)。**

      Pack 屬性在 LayoutKind.Auto 布局中無效。在 LayoutKind.Sequential 布局中,Pack 屬性用于指定字段的對齊要求及結(jié)構(gòu)體實(shí)例的對齊要求;在 LayoutKind.Explicit 布局中,Pack 屬性用于結(jié)構(gòu)體的對齊要求,會影響結(jié)構(gòu)體實(shí)例的末尾填充。

      LayoutKind.Sequential

      Pack 為 0 時(shí)等于默認(rèn)布局

      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Sequential, Pack = 0)]
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |   4-7: padding (4 bytes) |
      |--------------------------|
      |  8-15: Int64 b (8 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      

      Pack 不為 0 時(shí),取 Pack 和 字段類型大小 的較小值

      Pack 設(shè)置為 4 時(shí),intlong 字段的對齊要求都將被設(shè)置為 4 字節(jié),而 byte 字段的對齊要求仍然是 1 字節(jié)。結(jié)構(gòu)體的對齊要求是 4 字節(jié)。

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      {
          var foo = new Foo();
      
          // 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已經(jīng)定義
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Sequential, Pack = 4)]
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 782597876240    | 4    | True          | 195649469060 |
      | &foo.b          | 782597876244    | 8    | False         | 97824734530.5 |
      | &foo.c          | 782597876252    | 1    | True          | 782597876252 |
      Type layout for 'Foo'
      Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |  4-11: Int64 b (8 bytes) |
      |--------------------------|
      |    12: Byte c (1 byte)   |
      |--------------------------|
      | 13-15: padding (3 bytes) |
      |==========================|
      

      結(jié)構(gòu)體實(shí)例的起始地址按 8 字節(jié) 對齊(782597876240 / 8 = 97824734530)。但其大小取滿足 4 字節(jié)對齊要求的最小整數(shù)倍 16 字節(jié)( 末尾字段 c 的偏移量為 12,最小只能取到 16),并在末尾添加 3 字節(jié)的填充。

      Pack 設(shè)置為 1 時(shí),會形成密集的字段布局

      當(dāng) Pack 設(shè)置為 1 時(shí),所有字段的對齊要求都將被設(shè)置為 1 字節(jié),這意味著結(jié)構(gòu)體實(shí)例將按照 1 字節(jié)對齊。此時(shí),結(jié)構(gòu)體實(shí)例的字段將緊密排列,不會有額外的填充字節(jié)。

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      
      unsafe
      {
          // 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已經(jīng)定義
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Sequential, Pack = 1)]
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 302463314288    | 4    | True          | 75615828572  |
      | &foo.b          | 302463314292    | 8    | False         | 37807914286.5 |
      | &foo.c          | 302463314300    | 1    | True          | 302463314300 |
      Type layout for 'Foo'
      Size: 13 bytes. Paddings: 0 bytes (%0 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |  4-11: Int64 b (8 bytes) |
      |--------------------------|
      |    12: Byte c (1 byte)   |
      |==========================|
      

      起始地址為 8 的倍數(shù)(302463314288 / 8 = 37807914286),但結(jié)構(gòu)體實(shí)例的大小變?yōu)?13 字節(jié),沒有末尾填充。

      Pack 不為 0 的結(jié)構(gòu)體作為其他結(jié)構(gòu)體字段時(shí)

      如果外層的結(jié)構(gòu)體采用默認(rèn)字段布局則,則其實(shí)例的起始地址取決嵌套結(jié)構(gòu)體的最大字段默認(rèn)對齊要求,其實(shí)例大小取決該結(jié)構(gòu)體的最大字段對齊要求。

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&bar.foo.a);
          PrintPointerDetails(&bar.foo.b);
          PrintPointerDetails(&bar.foo.c);
          PrintPointerDetails(&bar.d);
      }
      
      TypeLayout.PrintLayout<Bar>();
      
      [StructLayout(LayoutKind.Sequential, Pack = 1)]
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      struct Bar
      {
          public Foo foo;
          public int d;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &bar.foo.a      | 724703897336    | 4    | True          | 181175974334 |
      | &bar.foo.b      | 724703897340    | 8    | False         | 90587987167.5 |
      | &bar.foo.c      | 724703897348    | 1    | True          | 724703897348 |
      | &bar.d          | 724703897352    | 4    | True          | 181175974338 |
      Type layout for 'Bar'
      Size: 20 bytes. Paddings: 3 bytes (%15 of empty space)
      |==============================|
      |  0-12: Foo foo (13 bytes)    |
      | |==========================| |
      | |   0-3: Int32 a (4 bytes) | |
      | |--------------------------| |
      | |  4-11: Int64 b (8 bytes) | |
      | |--------------------------| |
      | |    12: Byte c (1 byte)   | |
      | |==========================| |
      |------------------------------|
      | 13-15: padding (3 bytes)     |
      |------------------------------|
      | 16-19: Int32 d (4 bytes)     |
      |==============================|
      

      在上面的例子中,Bar 結(jié)構(gòu)體包含一個(gè) Foo 類型的字段 foo

      Bar 的實(shí)例起始地址也滿足 8 字節(jié)對齊要求(724703897336 / 8 = 90587987167)。

      foo 的對齊要求為 1 字節(jié), d 的對齊要求為 4 字節(jié),所以 Bar 的實(shí)例大小為 20 字節(jié)(4 的整數(shù)倍),并在food 之間添加了 3 字節(jié)的填充。

      LayoutKind.Explicit

      Pack 為 0 時(shí),結(jié)構(gòu)體按照最大字段默認(rèn)對齊要求對齊

      Explicit 布局中,我們需要顯式指定每個(gè)字段的偏移量。使用 FieldOffsetAttribute 來指定字段的偏移量。此時(shí)偏移量可以是任意值,甚至允許重疊字段。

      此時(shí)雖然字段地址可能由于是任意值而不滿足對齊要求,但結(jié)構(gòu)體實(shí)例的起始地址依舊按 8 字節(jié) 對齊,且結(jié)構(gòu)體實(shí)例的大小是最大字段對齊要求的整數(shù)倍。

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      
      unsafe
      {
          // 方法 PrintPointerHeader 和 PrintPointerDetails 在前言部分已經(jīng)定義
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Explicit, Pack = 0)]
      
      struct Foo
      {
          [FieldOffset(0)]
          public int a;
          [FieldOffset(3)]
          public long b;
          [FieldOffset(11)]
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 6095151432      | 4    | True          | 1523787858   |
      | &foo.b          | 6095151435      | 8    | False         | 761893929.38 |
      | &foo.c          | 6095151443      | 1    | True          | 6095151443   |
      Type layout for 'Foo'
      Size: 16 bytes. Paddings: 4 bytes (%25 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |  3-10: Int64 b (8 bytes) |
      |--------------------------|
      |    11: Byte c (1 byte)   |
      |--------------------------|
      | 12-15: padding (4 bytes) |
      |==========================|
      

      上面例子中,Foo 結(jié)構(gòu)體的字段 abc 的偏移量分別為 0、3 和 11。可以看到,雖然字段的地址不再滿足對齊要求,但結(jié)構(gòu)體實(shí)例的起始地址仍然按 8 字節(jié) 對齊(6095151432 / 8 = 761893929),且結(jié)構(gòu)體實(shí)例的大小為 16 字節(jié)(最大字段對齊要求的整數(shù)倍),末尾添加了 4 字節(jié)的填充。

      如果將 c 字段的偏移量改為 16,則結(jié)構(gòu)體實(shí)例的大小將變?yōu)?24 字節(jié),并且會在末尾添加 7 字節(jié)的填充。

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 6166536512      | 4    | True          | 1541634128   |
      | &foo.b          | 6166536515      | 8    | False         | 770817064.38 |
      | &foo.c          | 6166536528      | 1    | True          | 6166536528   |
      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 12 bytes (%50 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |  3-10: Int64 b (8 bytes) |
      |--------------------------|
      | 11-15: padding (5 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      

      Pack 不為 0 時(shí),結(jié)構(gòu)體實(shí)例按照 Pack 與 最大字段對齊要求 的較小值對齊

      Explicit 布局中,如果設(shè)置了 Pack 屬性且不為 0,則結(jié)構(gòu)體實(shí)例將按照 Pack 的值對齊。字段的偏移量仍然可以是任意值,但結(jié)構(gòu)體實(shí)例的大小將受到 Pack 屬性的影響。

      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Explicit, Pack = 4)]
      struct Foo
      {
          [FieldOffset(0)]
          public int a;
          [FieldOffset(5)]
          public long b;
          [FieldOffset(16)]
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 6122676544      | 4    | True          | 1530669136   |
      | &foo.b          | 6122676549      | 8    | False         | 765334568.63 |
      | &foo.c          | 6122676560      | 1    | True          | 6122676560   |
      Type layout for 'Foo'
      Size: 20 bytes. Paddings: 7 bytes (%35 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |     4: padding (1 byte)  |
      |--------------------------|
      |  5-12: Int64 b (8 bytes) |
      |--------------------------|
      | 13-15: padding (3 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-19: padding (3 bytes) |
      |==========================|
      

      在上面的例子中,由于 Pack 屬性設(shè)置為 4,與 long 類型的 8 字節(jié) 比較則結(jié)構(gòu)體實(shí)例應(yīng)按照 4 字節(jié) 對齊。因?yàn)?c 的偏移量為 16,所以 Foo 的大小在取值此時(shí)符合條件的 4 的最小整數(shù)倍后變?yōu)?20 字節(jié),并在末尾添加了 3 字節(jié) 的填充。

      改成 Pack = 128 后,結(jié)構(gòu)體實(shí)例的大小按照最大字段默認(rèn)對齊要求 8 字節(jié) 對齊。

      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Explicit, Pack = 128)]
      struct Foo
      {
          [FieldOffset(0)]
          public int a;
          [FieldOffset(5)]
          public long b;
          [FieldOffset(16)]
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 6104211776      | 4    | True          | 1526052944   |
      | &foo.b          | 6104211781      | 8    | False         | 763026472.63 |
      | &foo.c          | 6104211792      | 1    | True          | 6104211792   |
      Type layout for 'Foo'
      Size: 24 bytes. Paddings: 11 bytes (%45 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |     4: padding (1 byte)  |
      |--------------------------|
      |  5-12: Int64 b (8 bytes) |
      |--------------------------|
      | 13-15: padding (3 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |--------------------------|
      | 17-23: padding (7 bytes) |
      |==========================|
      

      將 Pack 屬性設(shè)置為 1 可以消除結(jié)構(gòu)體實(shí)例的末尾填充

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      [StructLayout(LayoutKind.Explicit, Pack = 1)]
      struct Foo
      {
          [FieldOffset(0)]
          public int a;
          [FieldOffset(5)]
          public long b;
          [FieldOffset(16)]
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 468685679112    | 4    | True          | 117171419778 |
      | &foo.b          | 468685679117    | 8    | False         | 58585709889.63 |
      | &foo.c          | 468685679128    | 1    | True          | 468685679128 |
      Type layout for 'Foo'
      Size: 17 bytes. Paddings: 4 bytes (%23 of empty space)
      |==========================|
      |   0-3: Int32 a (4 bytes) |
      |--------------------------|
      |     4: padding (1 byte)  |
      |--------------------------|
      |  5-12: Int64 b (8 bytes) |
      |--------------------------|
      | 13-15: padding (3 bytes) |
      |--------------------------|
      |    16: Byte c (1 byte)   |
      |==========================|
      

      此時(shí)實(shí)例的起始地址仍然按 8 字節(jié) 對齊(468685679112 / 8 = 58585709889),但實(shí)例的大小則是 17 字節(jié),末尾填充為 0 字節(jié)。

      Pack 屬性不為 0 的結(jié)構(gòu)體作為其他結(jié)構(gòu)體字段時(shí)

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      var bar = new Bar
      {
          foo = new Foo(),
          d = 4
      };
      
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&bar.foo.a);
          PrintPointerDetails(&bar.foo.b);
          PrintPointerDetails(&bar.foo.c);
          PrintPointerDetails(&bar.d);
      }
      
      TypeLayout.PrintLayout<Bar>();
      
      [StructLayout(LayoutKind.Explicit, Pack = 1)]
      struct Foo
      {
          [FieldOffset(0)]
          public int a;
          [FieldOffset(5)]
          public long b;
          [FieldOffset(16)]
          public byte c;
      }
      
      struct Bar
      {
          public Foo foo;
          public int d;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &bar.foo.a      | 967090628200    | 4    | True          | 241772657050 |
      | &bar.foo.b      | 967090628205    | 8    | False         | 120886328525.63 |
      | &bar.foo.c      | 967090628216    | 1    | True          | 967090628216 |
      | &bar.d          | 967090628220    | 4    | True          | 241772657055 |
      Type layout for 'Bar'
      Size: 24 bytes. Paddings: 7 bytes (%29 of empty space)
      |==============================|
      |  0-16: Foo foo (17 bytes)    |
      | |==========================| |
      | |   0-3: Int32 a (4 bytes) | |
      | |--------------------------| |
      | |     4: padding (1 byte)  | |
      | |--------------------------| |
      | |  5-12: Int64 b (8 bytes) | |
      | |--------------------------| |
      | | 13-15: padding (3 bytes) | |
      | |--------------------------| |
      | |    16: Byte c (1 byte)   | |
      | |==========================| |
      |------------------------------|
      | 17-19: padding (3 bytes)     |
      |------------------------------|
      | 20-23: Int32 d (4 bytes)     |
      |==============================|
      

      在上面的例子中,Bar 結(jié)構(gòu)體包含一個(gè) Foo 類型的字段 foo,由于 Foo 的最大字段對齊要求為 8 字節(jié),所以 Bar 的實(shí)例起始地址也滿足 8 字節(jié)對齊要求(967090628200 / 8 = 120886328525)。

      foo 的對齊要求為 1 字節(jié), d 的對齊要求為 4 字節(jié),所以 Bar 的實(shí)例大小為 24 字節(jié)(4 的整數(shù)倍),并在food 之間添加了 3 字節(jié)的填充。

      LayoutKind.Auto

      使用 LayoutKind.Auto 時(shí),runtime 將根據(jù)字段的類型和聲明順序自動確定字段的布局,會調(diào)整實(shí)例字段的排列順序和對齊要求,以優(yōu)化內(nèi)存布局和性能。

      LayoutKind.Auto 也是引用類型實(shí)例字段的默認(rèn)布局方式。

      using System.Runtime.CompilerServices;
      using System.Runtime.InteropServices;
      using ObjectLayoutInspector;
      
      var foo = new Foo
      {
          a = 1,
          b = 2,
          c = 3
      };
      
      unsafe
      {
          PrintPointerHeader();
          PrintPointerDetails(&foo.a);
          PrintPointerDetails(&foo.b);
          PrintPointerDetails(&foo.c);
      }
      
      TypeLayout.PrintLayout<Foo>();
      
      
      [StructLayout(LayoutKind.Auto)]
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &foo.a          | 6166815056      | 4    | True          | 1541703764   |
      | &foo.b          | 6166815048      | 8    | True          | 770851881    |
      | &foo.c          | 6166815060      | 1    | True          | 6166815060   |
      Type layout for 'Foo'
      Size: 16 bytes. Paddings: 3 bytes (%18 of empty space)
      |==========================|
      |   0-7: Int64 b (8 bytes) |
      |--------------------------|
      |  8-11: Int32 a (4 bytes) |
      |--------------------------|
      |    12: Byte c (1 byte)   |
      |--------------------------|
      | 13-15: padding (3 bytes) |
      |==========================|
      

      上面例子中,Foo 結(jié)構(gòu)體的字段 b 被放在了前面,各字段都按照其類型大小進(jìn)行了對齊,相較于默認(rèn)布局,Foo 結(jié)構(gòu)體的內(nèi)存布局更加緊湊,減少了填充字節(jié)的數(shù)量。

      等效于于下面的結(jié)構(gòu)體定義

      [StructLayout(LayoutKind.Sequential, Pack = 0)]
      struct Foo
      {
          public long b;
          public int a;
          public byte c;
      }
      

      作為數(shù)組元素時(shí)的結(jié)構(gòu)體實(shí)例

      默認(rèn)字段布局

      默認(rèn)布局的結(jié)構(gòu)體實(shí)例在數(shù)組中也會按照最大字段對齊要求進(jìn)行對齊。每個(gè)結(jié)構(gòu)體實(shí)例的起始地址都是該結(jié)構(gòu)體最大字段對齊要求的整數(shù)倍。

      因此默認(rèn)布局下,數(shù)組中的每個(gè)結(jié)構(gòu)體的字段都是滿足對齊要求的。

      using System.Runtime.CompilerServices;
      
      unsafe
      {
          // 讀者也可以替換成堆上分配的數(shù)組來查看運(yùn)行結(jié)果
          var arr = stackalloc Foo[] { new Foo(), new Foo() };
      
          PrintPointerHeader();
          
          PrintPointerDetails(&arr[0].a);
          PrintPointerDetails(&arr[0].b);
          PrintPointerDetails(&arr[0].c);
          PrintPointerDetails(&arr[1].a);
          PrintPointerDetails(&arr[1].b);
          PrintPointerDetails(&arr[1].c);
      }
      
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &arr[0].a       | 1029625933216   | 4    | True          | 257406483304 |
      | &arr[0].b       | 1029625933224   | 8    | True          | 128703241653 |
      | &arr[0].c       | 1029625933232   | 1    | True          | 1029625933232 |
      | &arr[1].a       | 1029625933240   | 4    | True          | 257406483310 |
      | &arr[1].b       | 1029625933248   | 8    | True          | 128703241656 |
      | &arr[1].c       | 1029625933256   | 1    | True          | 1029625933256 |
      

      非默認(rèn)字段布局

      因?yàn)閿?shù)組中結(jié)構(gòu)體實(shí)例是連續(xù)存儲的,如果結(jié)構(gòu)體實(shí)例的字段布局進(jìn)行了非默認(rèn)的調(diào)整,則可能導(dǎo)致第二個(gè)開始的構(gòu)體實(shí)例完全不滿足對齊要求(包括實(shí)例的起始地址和字段地址)。

      using System.Runtime.CompilerServices;
      
      unsafe
      {
          // 讀者也可以替換成堆上分配的數(shù)組來查看運(yùn)行結(jié)果
          var arr = stackalloc Foo[] { new Foo(), new Foo() };
      
          PrintPointerHeader();
          
          PrintPointerDetails(&arr[0].a);
          PrintPointerDetails(&arr[0].b);
          PrintPointerDetails(&arr[0].c);
          PrintPointerDetails(&arr[1].a);
          PrintPointerDetails(&arr[1].b);
          PrintPointerDetails(&arr[1].c);
      }
      
      [StructLayout(LayoutKind.Sequential, Pack = 1)]
      struct Foo
      {
          public int a;
          public long b;
          public byte c;
      }
      
      | Expr            | Address         | Size | AlignedBySize | Addr/Size    |
      | &arr[0].a       | 654696769936    | 4    | True          | 163674192484 |
      | &arr[0].b       | 654696769940    | 8    | False         | 81837096242.5 |
      | &arr[0].c       | 654696769948    | 1    | True          | 654696769948 |
      | &arr[1].a       | 654696769949    | 4    | False         | 163674192487.25 |
      | &arr[1].b       | 654696769953    | 8    | False         | 81837096244.13 |
      | &arr[1].c       | 654696769961    | 1    | True          | 654696769961 |
      
      posted @ 2025-06-05 19:45  黑洞視界  閱讀(1639)  評論(14)    收藏  舉報(bào)
      主站蜘蛛池模板: 人妻一区二区三区三区| 成人亚洲欧美一区二区三区| 福利视频一区二区在线| 亚洲伊人成无码综合网| 精品久久久bbbb人妻| 亚洲午夜无码久久久久蜜臀av| 国产人妻人伦精品1国产丝袜| 欧美成人h亚洲综合在线观看| 盐山县| 日韩av裸体在线播放| 亚洲午夜精品毛片成人播放| 久久国产精品成人影院| 久久久婷婷成人综合激情| 26uuu另类亚洲欧美日本| 亚洲熟女乱一区二区三区| 四房播色综合久久婷婷| 高潮迭起av乳颜射后入| 久久精品国产99精品亚洲| 在线看国产精品自拍内射| 亚洲AV乱码毛片在线播放| 久草热大美女黄色片免费看| 沁源县| 无码日韩精品一区二区三区免费| 无码抽搐高潮喷水流白浆| 日韩av一区二区精品不卡| 国产福利微视频一区二区| 国产精品亚洲国际在线看| 免费观看日本污污ww网站69| 9lporm自拍视频区| 无码日韩精品一区二区三区免费| 亚洲精品一品区二品区三品区| 伊人春色激情综合激情网| 精品久久人人做爽综合| 精品久久久久久无码专区不卡| 日韩精品一区二区三区久| 国产精品国三级国产av| 无码人妻丰满熟妇啪啪| 99久久精品国产一区色| 国产精品人妻久久无码不卡| 久久精品一本到99热免费| 国产超碰无码最新上传|