同事分享了一記讓 32 位元 .NET 程式突破 2GB 記憶體上限的密技,讓我不禁獻上了膝蓋,當然要轉分享一下。
.NET 編譯成 32 位元與 64 位元最大的差異在於可用記憶體上限,32 位元的記憶體定址上限為 4GB,其中 2GB 配置給作業系統核心模式,應用程式為使用者模式只有 2GB 可用,實際執行需再扣除 Runtime 本身耗用的記憶體,依經驗只能用到 1.6GB 左右。所以若無特殊限制,程式最好編譯成 AnyCPU 或 x64 以充分享用記憶體。但實務上 .NET 程式一旦引用了 32 位元 Unmanaged 元件,就毫無選擇只能以 32 位元執行。
Windows 有個 /3GB 啟動參數,可調整只配置 1GB 給核心模式,留下 3GB 給應用程式使用,但 /3GB 的設定步驟繁瑣,要部署大量客戶端很有麻煩。Visual C++ 有個 EDITBIN 命令列工具,可修改 OBJ/DLL/EXE 檔案旗標,其中有個 /LARGEADDRESSAWARE 參數可針對特定 EXE 開放 3GB 模式,突破 1.6GB 上限。
以下是個簡單的測試程式,透過不斷產生 1M 長度字串消耗記憶體直接 OutOfMemoryException,並用以前介紹過的記憶體用量觀察函式測量佔用的 Managed Heap 記憶體:
using System;
using System.Collections.Generic;
namespace _32BitAppTest
{
public class Program
{
static void Main(string[] args)
{
try
{
CreateBigData();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
Console.WriteLine("Press any key for exit... ");
Console.ReadKey();
}
static void DumpMemSize(int count)
{
//強制回收記憶體清出空間,以充分利用所有記憶體
var memSz = GC.GetTotalMemory(true) / 1024 / 1024;
Console.WriteLine(
$"Managed Heap={memSz}MB, Count={count}");
}
static void CreateBigData()
{
var dic = new Dictionary<long, string>();
var src = new string(' ', 1024 * 1024 - 1);
for (int i = 0; i < 4096; i++)
{
dic.Add(i, src + (i % 10));
if (i % 32 == 0)
DumpMemSize(i);
}
}
}
}
實測結果,大約用到 1.5GB 左右出現 OutOfMemoryException,跟一般認知的 2GB 上限相近。
用 EDITBIN /LARGEADDRESSAWARE 32BitAppTest.exe 開光後,同一支 32 位元 .NET程式便能吃到 3GB 記憶體,神奇吧!
如果要每次編譯後自動修改,可加在專案 Post-Build Event,以下是適用 VS2015/VS2017 的寫法:
使用以下腳本則可適用多個 VS 版本:參考
IF EXIST "%VS140COMNTOOLS%" CALL "%VS140COMNTOOLS%vsvars32.bat"
IF EXIST "%VS120COMNTOOLS%" CALL "%VS120COMNTOOLS%vsvars32.bat"
IF EXIST "%VS110COMNTOOLS%" CALL "%VS110COMNTOOLS%vsvars32.bat"
IF EXIST "%VS100COMNTOOLS%" CALL "%VS100COMNTOOLS%vsvars32.bat"
editbin.exe /LARGEADDRESSAWARE $(TargetPath)
補充,EDITBIN 為 Visual C++ 附屬工具,Visual Studio 記得要安裝 Visual C++ 才有的用。
【同場加映】
C/C++ Build Tools 有另一件工具 - DUMPBIN,可檢查 EXE 是否已設定 LARGEADDRESSAWARE 旗標,如下圖: