为什么代码中的小改动不会影响 exe 文件的大小?

Why don't small changes in code affect exe file size?

我很好奇 - 有时我会更改我的代码,重新编译,然后将我的 exe 或 dll 文件复制到旧版本上,然后看到 Windows 告诉我文件的日期已更改,但是大小保持完全相同。这是为什么?

例如,我使用以下控制台应用程序进行了测试:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication4
{
    class Program
    {
        static void Main(string[] args)
        {
            int a = 1;
            int b = 2;
            Console.WriteLine(a + b);
        }
    }
}

这生成了一个 5120 字节的 exe 文件(Visual Studio 2012,调试版本)。然后,我将代码更改为:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication4
{
    class Program
    {
        static void Main(string[] args)
        {
            int a = 1;
            int b = 2;
            int c = 3;
            Console.WriteLine(a + b + c);
        }
    }
}

exe大小完全一样

我看了一下反汇编,它显示了 IL 代码中的差异,所以差异不可能被优化掉:

第一个版本:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       15 (0xf)
  .maxstack  2
  .locals init (int32 V_0,
           int32 V_1)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  ldc.i4.2
  IL_0004:  stloc.1
  IL_0005:  ldloc.0
  IL_0006:  ldloc.1
  IL_0007:  add
  IL_0008:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_000d:  nop
  IL_000e:  ret
} // end of method Program::Main

第二个版本:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 a,
           [1] int32 b,
           [2] int32 c)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  ldc.i4.2
  IL_0004:  stloc.1
  IL_0005:  ldc.i4.3
  IL_0006:  stloc.2
  IL_0007:  ldloc.0
  IL_0008:  ldloc.1
  IL_0009:  add
  IL_000a:  ldloc.2
  IL_000b:  add
  IL_000c:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0011:  nop
  IL_0012:  ret
} // end of method Program::Main

如果代码体积更大,文件大小怎么可能完全一样?这只是偶然的机会吗?这在我身上经常发生(在对代码进行小改动时)...

可执行文件包含多个部分。如果我没记错的话,每个部分都对齐到 512 字节。

来自https://msdn.microsoft.com/en-us/library/ms809762.aspx

DWORD FileAlignment

In the PE file, the raw data that comprises each section is guaranteed to start at a multiple of this value. The default value is 0x200 bytes, probably to ensure that sections always start at the beginning of a disk sector (which are also 0x200 bytes in length). This field is equivalent to the segment/resource alignment size in NE files. Unlike NE files, PE files typically don't have hundreds of sections, so the space wasted by aligning the file sections is almost always very small.

编辑:磁盘上的所有部分大小都向上舍入(填充)为 FileAlignment 的倍数。来自 http://www.openwatcom.org/ftp/devel/docs/pecoff.pdf

SizeOfRawData

Size of the section (object file) or size of the initialized data on disk (image files). For executable image, this must be a multiple of FileAlignment from the optional header. If this is less than VirtualSize the remainder of the section is zero filled. Because this field is rounded while the VirtualSize field is not it is possible for this to be greater than VirtualSize as well. When a section contains only uninitialized data, this field should be 0.

我认为即使是最后一部分也被填充得如此之多,这样发出部分的链接器代码和加载它们的加载器代码就不必担心最后一部分大小的特殊情况。无论如何,对 trim 最后一部分的优化将是一个毫无意义的优化,因为磁盘扇区(以及该文件系统的更大集群的顶部)有 internal fragmentation 会吞噬任何此类“节省”(来自 trim大部分时间都是最后一节。