3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#で2GB以上のメモリを確保する方法

3
Posted at

はじめに

C#でメモリを確保する場合、byte[]配列やMarshal.AllocHGlobalなどを使いますが、これらのメソッドはメモリサイズの指定にint型を使用しています。int型の最大値は約2,147,483,647バイト(約2GB)であるため、プロジェクトの設定で64bitアプリケーションに設定しても、2GB以上のメモリを確保することができません。

例えば、以下のコードはいずれも2GB以上を確保できません。

// byte配列 → int型でサイズ指定 → 2GB以上は不可
byte[] buffer = new byte[3000000000]; // コンパイルエラーまたはOverflowException

// Marshal.AllocHGlobal → int型でサイズ指定 → 2GB以上は不可
IntPtr ptr = Marshal.AllocHGlobal(3000000000); // コンパイルエラー

画像処理など大量のメモリを必要とする場面では、この制限が問題になることがあります。

参考ページ: C#のメモリ制限について

解決策

Win32APIの _aligned_malloc 関数を使うことで、2GB以上のメモリを確保することができます。_aligned_mallocはC言語のランタイムライブラリ(msvcrt.dll)に含まれている関数で、指定したアラインメント境界に揃えたメモリを確保します。

// _aligned_mallocのシグネチャ(C言語)
void* _aligned_malloc(size_t size, size_t alignment);

サイズの指定にsize_t型(64bit環境では8バイト)を使用するため、2GB以上のメモリを確保できます。

ただし、_aligned_mallocで確保したメモリはアンマネージメモリです。C#のガベージコレクタが自動で解放してくれないため、使い終わったら必ず_aligned_freeで解放する必要があります。解放し忘れるとメモリリークが発生します。

そこで、_aligned_malloc_aligned_freeをラップした AlignedMemoryクラス を作成し、IDisposableパターンを実装することで、安全にメモリを管理できるようにします。

AlignedMemoryクラスの実装

クラス全体のコード

using System;
using System.Runtime.InteropServices;

namespace AlignedMemoryLib
{
    /// <summary>
    /// _aligned_mallocを使って2GB以上のメモリを確保するクラス
    /// </summary>
    public class AlignedMemory : IDisposable
    {
        // Win32APIの_aligned_mallocと_aligned_freeをインポート
        [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr _aligned_malloc(ulong size, ulong alignment);

        [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern void _aligned_free(IntPtr memblock);

        private IntPtr _pointer = IntPtr.Zero;
        private readonly ulong _size;
        private bool _disposed = false;

        /// <summary>
        /// 確保したメモリの先頭ポインタ
        /// </summary>
        public IntPtr Pointer => _pointer;

        /// <summary>
        /// 確保したメモリのサイズ(バイト)
        /// </summary>
        public ulong Size => _size;

        /// <summary>
        /// コンストラクタ。指定サイズのアラインメントされたメモリを確保する。
        /// </summary>
        /// <param name="size">確保するメモリサイズ(バイト)</param>
        /// <param name="alignment">アラインメント境界(デフォルト: 16バイト)</param>
        public AlignedMemory(ulong size, ulong alignment = 16)
        {
            _size = size;
            _pointer = _aligned_malloc(size, alignment);
            if (_pointer == IntPtr.Zero)
                throw new OutOfMemoryException(
                    $"{size} バイトのアラインメントメモリの確保に失敗しました。");
        }

        // --- Read系メソッド ---

        /// <summary>
        /// 指定オフセットから1バイト読み込む
        /// </summary>
        public byte ReadByte(ulong offset)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(byte));
            return Marshal.ReadByte(new IntPtr(_pointer.ToInt64() + (long)offset));
        }

        /// <summary>
        /// 指定オフセットから2バイト(Int16)読み込む
        /// </summary>
        public short ReadInt16(ulong offset)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(short));
            return Marshal.ReadInt16(new IntPtr(_pointer.ToInt64() + (long)offset));
        }

        /// <summary>
        /// 指定オフセットから4バイト(Int32)読み込む
        /// </summary>
        public int ReadInt32(ulong offset)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(int));
            return Marshal.ReadInt32(new IntPtr(_pointer.ToInt64() + (long)offset));
        }

        /// <summary>
        /// 指定オフセットから8バイト(Int64)読み込む
        /// </summary>
        public long ReadInt64(ulong offset)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(long));
            return Marshal.ReadInt64(new IntPtr(_pointer.ToInt64() + (long)offset));
        }

        // --- Write系メソッド ---

        /// <summary>
        /// 指定オフセットに1バイト書き込む
        /// </summary>
        public void WriteByte(ulong offset, byte value)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(byte));
            Marshal.WriteByte(new IntPtr(_pointer.ToInt64() + (long)offset), value);
        }

        /// <summary>
        /// 指定オフセットに2バイト(Int16)書き込む
        /// </summary>
        public void WriteInt16(ulong offset, short value)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(short));
            Marshal.WriteInt16(new IntPtr(_pointer.ToInt64() + (long)offset), value);
        }

        /// <summary>
        /// 指定オフセットに4バイト(Int32)書き込む
        /// </summary>
        public void WriteInt32(ulong offset, int value)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(int));
            Marshal.WriteInt32(new IntPtr(_pointer.ToInt64() + (long)offset), value);
        }

        /// <summary>
        /// 指定オフセットに8バイト(Int64)書き込む
        /// </summary>
        public void WriteInt64(ulong offset, long value)
        {
            ThrowIfDisposed();
            ThrowIfOutOfRange(offset, sizeof(long));
            Marshal.WriteInt64(new IntPtr(_pointer.ToInt64() + (long)offset), value);
        }

        // --- IDisposableパターン ---

        /// <summary>
        /// メモリを解放する
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// メモリを解放する(内部実装)
        /// </summary>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (_pointer != IntPtr.Zero)
                {
                    _aligned_free(_pointer);
                    _pointer = IntPtr.Zero;
                }
                _disposed = true;
            }
        }

        /// <summary>
        /// ファイナライザ。Disposeし忘れた場合の安全策。
        /// </summary>
        ~AlignedMemory()
        {
            Dispose(false);
        }

        // --- ヘルパーメソッド ---

        /// <summary>
        /// 既にDisposeされていないかチェック
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (_disposed)
                throw new ObjectDisposedException(nameof(AlignedMemory));
        }

        /// <summary>
        /// オフセットが範囲内かチェック
        /// </summary>
        private void ThrowIfOutOfRange(ulong offset, int typeSize)
        {
            if (offset + (ulong)typeSize > _size)
                throw new ArgumentOutOfRangeException(nameof(offset),
                    $"オフセット {offset} + {typeSize} バイトがメモリサイズ {_size} を超えています。");
        }
    }
}

コードのポイント

DllImportによるWin32API呼び出し

msvcrt.dllに含まれる_aligned_malloc_aligned_freeをP/Invokeで呼び出しています。サイズとアラインメントの型をulongにすることで、2GB以上の値を指定できます。

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr _aligned_malloc(ulong size, ulong alignment);

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void _aligned_free(IntPtr memblock);

IDisposableパターンとファイナライザ

IDisposableパターンを実装することで、using文を使った安全なメモリ管理ができます。さらにファイナライザ(デストラクタ)を実装しておくことで、万が一Disposeを呼び忘れた場合でも、ガベージコレクション時にメモリが解放されます。

// using文で安全にメモリを管理
using (var memory = new AlignedMemory(3_000_000_000))
{
    // メモリを使用する処理
} // ここで自動的にDisposeされる

ulongオフセットによるアクセス

Marshal.ReadByte等のメソッドはIntPtrでアドレスを受け取るため、ulongのオフセットを使ったポインタ演算でアクセスします。これにより、2GBを超える位置にもアクセスできます。

// 2GB以上のオフセットにもアクセス可能
return Marshal.ReadByte(new IntPtr(_pointer.ToInt64() + (long)offset));

.NET Frameworkと.NET 6+の違い

.NET Framework(従来の方法)

.NET Frameworkでは、DllImport属性を使ってネイティブ関数をインポートします。上記のコードはこの方法を使っています。

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr _aligned_malloc(ulong size, ulong alignment);

.NET 6以降(LibraryImport)

.NET 7以降では、LibraryImport属性とソースジェネレータを使った新しい方法が利用できます。partialメソッドとして宣言する点が異なります。

using System.Runtime.InteropServices;

// .NET 7以降で利用可能
[LibraryImport("msvcrt.dll", EntryPoint = "_aligned_malloc")]
[UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
private static partial IntPtr AlignedMalloc(ulong size, ulong alignment);

[LibraryImport("msvcrt.dll", EntryPoint = "_aligned_free")]
[UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
private static partial void AlignedFree(IntPtr memblock);

LibraryImportはコンパイル時にマーシャリングコードを生成するため、DllImportよりもパフォーマンスが向上する場合があります。ただし、本記事のように単純な型のみを扱う場合は大きな差はありません。

どちらの方法でも動作に違いはありませんので、お使いの環境に合わせて選択してください。

プロジェクトの設定(重要)

2GB以上のメモリを扱うには、64bitアプリケーションとしてビルドする必要があります。以下のいずれかの設定を行ってください。

方法1: プラットフォームをx64に設定する

プロジェクトのプロパティ → ビルド → プラットフォームターゲットを x64 に変更する。

方法2: AnyCPUの場合は「32ビットの優先」を外す

プロジェクトのプロパティ → ビルド → 「32ビットの優先」のチェックを必ず外す。

「32ビットの優先」にチェックが入っていると、64bit OSでも32bitプロセスとして実行されるため、2GB以上のメモリを確保できません。

サンプル1: 3GB確保デモ(コンソールアプリケーション)

3GBのメモリを確保し、先頭と2GBを超えた位置にデータを書き込み・読み込みするサンプルです。

using System;
using AlignedMemoryLib;

namespace AlignedMemoryConsoleDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 3GB = 3,221,225,472 バイト
            ulong size = 3UL * 1024 * 1024 * 1024;

            Console.WriteLine($"確保するメモリサイズ: {size} バイト ({size / (1024.0 * 1024 * 1024):F1} GB)");
            Console.WriteLine();

            try
            {
                using (var memory = new AlignedMemory(size))
                {
                    Console.WriteLine("メモリの確保に成功しました。");
                    Console.WriteLine($"ポインタ: 0x{memory.Pointer.ToInt64():X16}");
                    Console.WriteLine();

                    // --- 先頭付近への書き込みと読み込み ---
                    Console.WriteLine("=== 先頭付近のテスト ===");

                    memory.WriteByte(0, 0xAB);
                    byte valueByte = memory.ReadByte(0);
                    Console.WriteLine($"オフセット 0 に 0xAB を書き込み → 読み込み: 0x{valueByte:X2}");

                    memory.WriteInt32(4, 123456789);
                    int valueInt32 = memory.ReadInt32(4);
                    Console.WriteLine($"オフセット 4 に 123456789 を書き込み → 読み込み: {valueInt32}");

                    Console.WriteLine();

                    // --- 2GBを超えた位置への書き込みと読み込み ---
                    Console.WriteLine("=== 2GB超の位置のテスト ===");

                    // 2GB + 100 の位置
                    ulong offset2GB = 2UL * 1024 * 1024 * 1024 + 100;

                    memory.WriteByte(offset2GB, 0xCD);
                    byte valueByte2 = memory.ReadByte(offset2GB);
                    Console.WriteLine($"オフセット {offset2GB} (2GB+100) に 0xCD を書き込み → 読み込み: 0x{valueByte2:X2}");

                    memory.WriteInt64(offset2GB + 8, 9876543210L);
                    long valueInt64 = memory.ReadInt64(offset2GB + 8);
                    Console.WriteLine($"オフセット {offset2GB + 8} (2GB+108) に 9876543210 を書き込み → 読み込み: {valueInt64}");

                    // 3GB直前の位置
                    ulong offsetNearEnd = size - 8;
                    memory.WriteInt64(offsetNearEnd, long.MaxValue);
                    long valueNearEnd = memory.ReadInt64(offsetNearEnd);
                    Console.WriteLine($"オフセット {offsetNearEnd} (末尾-8) に {long.MaxValue} を書き込み → 読み込み: {valueNearEnd}");

                    Console.WriteLine();
                    Console.WriteLine("すべてのテストが正常に完了しました。");
                }

                // usingブロックを抜けた時点でメモリは自動的に解放される
                Console.WriteLine("メモリを解放しました。");
            }
            catch (OutOfMemoryException ex)
            {
                Console.WriteLine($"メモリの確保に失敗しました: {ex.Message}");
                Console.WriteLine("十分な空きメモリがあるか確認してください。");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"エラーが発生しました: {ex.Message}");
            }

            Console.WriteLine();
            Console.WriteLine("Enterキーを押して終了...");
            Console.ReadLine();
        }
    }
}

実行結果の例

確保するメモリサイズ: 3221225472 バイト (3.0 GB)

メモリの確保に成功しました。
ポインタ: 0x0000020000000000

=== 先頭付近のテスト ===
オフセット 0 に 0xAB を書き込み → 読み込み: 0xAB
オフセット 4 に 123456789 を書き込み → 読み込み: 123456789

=== 2GB超の位置のテスト ===
オフセット 2147483748 (2GB+100) に 0xCD を書き込み → 読み込み: 0xCD
オフセット 2147483756 (2GB+108) に 9876543210 を書き込み → 読み込み: 9876543210
オフセット 3221225464 (末尾-8) に 9223372036854775807 を書き込み → 読み込み: 9223372036854775807

すべてのテストが正常に完了しました。
メモリを解放しました。

Enterキーを押して終了...

このサンプルを実行するには、64bitアプリケーションとしてビルドし、十分な空きメモリ(3GB以上)が必要です。プロジェクトのプロパティで「32ビットの優先」のチェックを外してください。

サンプル2: 画像バッファとしての利用(WinFormsアプリケーション)

大きなメモリ領域を画像バッファとして扱うサンプルです。メモリを確保し、ピクセルデータの書き込み・読み込みを行い、結果をフォーム上に表示します。

using System;
using System.Windows.Forms;
using AlignedMemoryLib;

namespace AlignedMemoryWinFormsDemo
{
    public partial class Form1 : Form
    {
        private AlignedMemory _memory;

        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// メモリ確保ボタンのクリックイベント
        /// </summary>
        private void buttonAllocate_Click(object sender, EventArgs e)
        {
            // 既に確保済みの場合は解放
            if (_memory != null)
            {
                _memory.Dispose();
                _memory = null;
            }

            try
            {
                // 確保サイズをテキストボックスから取得(MB単位)
                if (!ulong.TryParse(textBoxSizeMB.Text, out ulong sizeMB))
                {
                    MessageBox.Show("サイズを正しく入力してください。", "エラー",
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }

                ulong sizeBytes = sizeMB * 1024 * 1024;
                _memory = new AlignedMemory(sizeBytes);

                AppendLog($"メモリを確保しました: {sizeMB} MB ({sizeBytes} バイト)");
                AppendLog($"ポインタ: 0x{_memory.Pointer.ToInt64():X16}");

                // ボタンの有効/無効を切り替え
                buttonWrite.Enabled = true;
                buttonRead.Enabled = true;
                buttonFree.Enabled = true;
            }
            catch (OutOfMemoryException ex)
            {
                AppendLog($"メモリの確保に失敗しました: {ex.Message}");
                MessageBox.Show("メモリの確保に失敗しました。\nサイズを小さくするか、空きメモリを確認してください。",
                    "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        /// <summary>
        /// ピクセル値書き込みボタンのクリックイベント
        /// </summary>
        private void buttonWrite_Click(object sender, EventArgs e)
        {
            if (_memory == null)
            {
                AppendLog("メモリが確保されていません。");
                return;
            }

            try
            {
                AppendLog("ピクセルデータの書き込みを開始...");

                // 画像バッファとして扱う(例: 幅, 高さ, 3チャンネル(RGB))
                int width = 1024;
                int height = 1024;
                int channels = 3; // RGB
                ulong imageSize = (ulong)(width * height * channels);

                // メモリサイズが足りるかチェック
                if (imageSize > _memory.Size)
                {
                    AppendLog($"メモリサイズが不足しています。必要: {imageSize} バイト, 確保済み: {_memory.Size} バイト");
                    return;
                }

                // グラデーションパターンを書き込む
                for (int y = 0; y < height; y++)
                {
                    for (int x = 0; x < width; x++)
                    {
                        ulong offset = (ulong)((y * width + x) * channels);

                        // R: 横方向のグラデーション
                        _memory.WriteByte(offset, (byte)(x * 255 / width));
                        // G: 縦方向のグラデーション
                        _memory.WriteByte(offset + 1, (byte)(y * 255 / height));
                        // B: 固定値
                        _memory.WriteByte(offset + 2, 128);
                    }
                }

                AppendLog($"書き込み完了: {width}x{height} ピクセル ({imageSize} バイト)");

                // 2GBを超える位置にも書き込み(メモリサイズが十分な場合)
                ulong offset2GB = 2UL * 1024 * 1024 * 1024;
                if (offset2GB + 3 <= _memory.Size)
                {
                    _memory.WriteByte(offset2GB, 255);     // R
                    _memory.WriteByte(offset2GB + 1, 0);   // G
                    _memory.WriteByte(offset2GB + 2, 0);   // B
                    AppendLog($"2GB超の位置 (オフセット {offset2GB}) にピクセル値 (255,0,0) を書き込みました。");
                }
            }
            catch (Exception ex)
            {
                AppendLog($"書き込みエラー: {ex.Message}");
            }
        }

        /// <summary>
        /// ピクセル値読み込みボタンのクリックイベント
        /// </summary>
        private void buttonRead_Click(object sender, EventArgs e)
        {
            if (_memory == null)
            {
                AppendLog("メモリが確保されていません。");
                return;
            }

            try
            {
                AppendLog("ピクセルデータの読み込みを開始...");

                int width = 1024;
                int channels = 3;

                // 先頭のピクセル値を読み込み
                byte r0 = _memory.ReadByte(0);
                byte g0 = _memory.ReadByte(1);
                byte b0 = _memory.ReadByte(2);
                AppendLog($"ピクセル(0,0): R={r0}, G={g0}, B={b0}");

                // 中央付近のピクセル値を読み込み
                ulong midOffset = (ulong)(512 * width + 512) * (ulong)channels;
                if (midOffset + 2 < _memory.Size)
                {
                    byte rMid = _memory.ReadByte(midOffset);
                    byte gMid = _memory.ReadByte(midOffset + 1);
                    byte bMid = _memory.ReadByte(midOffset + 2);
                    AppendLog($"ピクセル(512,512): R={rMid}, G={gMid}, B={bMid}");
                }

                // 最後の行のピクセル値を読み込み
                ulong lastOffset = (ulong)(1023 * width + 1023) * (ulong)channels;
                if (lastOffset + 2 < _memory.Size)
                {
                    byte rLast = _memory.ReadByte(lastOffset);
                    byte gLast = _memory.ReadByte(lastOffset + 1);
                    byte bLast = _memory.ReadByte(lastOffset + 2);
                    AppendLog($"ピクセル(1023,1023): R={rLast}, G={gLast}, B={bLast}");
                }

                // 2GBを超える位置のピクセル値を読み込み(メモリサイズが十分な場合)
                ulong offset2GB = 2UL * 1024 * 1024 * 1024;
                if (offset2GB + 3 <= _memory.Size)
                {
                    byte r2GB = _memory.ReadByte(offset2GB);
                    byte g2GB = _memory.ReadByte(offset2GB + 1);
                    byte b2GB = _memory.ReadByte(offset2GB + 2);
                    AppendLog($"2GB超の位置 (オフセット {offset2GB}): R={r2GB}, G={g2GB}, B={b2GB}");
                }
                else
                {
                    AppendLog("2GB超の位置を読み込むには、3072MB以上のメモリを確保してください。");
                }

                AppendLog("読み込み完了。");
            }
            catch (Exception ex)
            {
                AppendLog($"読み込みエラー: {ex.Message}");
            }
        }

        /// <summary>
        /// メモリ解放ボタンのクリックイベント
        /// </summary>
        private void buttonFree_Click(object sender, EventArgs e)
        {
            if (_memory != null)
            {
                _memory.Dispose();
                _memory = null;
                AppendLog("メモリを解放しました。");

                buttonWrite.Enabled = false;
                buttonRead.Enabled = false;
                buttonFree.Enabled = false;
            }
        }

        /// <summary>
        /// ログをテキストボックスに追加
        /// </summary>
        private void AppendLog(string message)
        {
            textBoxLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}");
        }

        /// <summary>
        /// フォームを閉じるときにメモリを解放
        /// </summary>
        protected override void OnFormClosed(FormClosedEventArgs e)
        {
            if (_memory != null)
            {
                _memory.Dispose();
                _memory = null;
            }
            base.OnFormClosed(e);
        }
    }
}

Form1.Designer.csのコード

namespace AlignedMemoryWinFormsDemo
{
    partial class Form1
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.labelSize = new System.Windows.Forms.Label();
            this.textBoxSizeMB = new System.Windows.Forms.TextBox();
            this.labelMB = new System.Windows.Forms.Label();
            this.buttonAllocate = new System.Windows.Forms.Button();
            this.buttonWrite = new System.Windows.Forms.Button();
            this.buttonRead = new System.Windows.Forms.Button();
            this.buttonFree = new System.Windows.Forms.Button();
            this.textBoxLog = new System.Windows.Forms.TextBox();
            this.SuspendLayout();
            //
            // labelSize
            //
            this.labelSize.AutoSize = true;
            this.labelSize.Location = new System.Drawing.Point(12, 15);
            this.labelSize.Name = "labelSize";
            this.labelSize.Size = new System.Drawing.Size(61, 12);
            this.labelSize.Text = "メモリサイズ:";
            //
            // textBoxSizeMB
            //
            this.textBoxSizeMB.Location = new System.Drawing.Point(79, 12);
            this.textBoxSizeMB.Name = "textBoxSizeMB";
            this.textBoxSizeMB.Size = new System.Drawing.Size(80, 19);
            this.textBoxSizeMB.Text = "3072";
            //
            // labelMB
            //
            this.labelMB.AutoSize = true;
            this.labelMB.Location = new System.Drawing.Point(165, 15);
            this.labelMB.Name = "labelMB";
            this.labelMB.Size = new System.Drawing.Size(21, 12);
            this.labelMB.Text = "MB";
            //
            // buttonAllocate
            //
            this.buttonAllocate.Location = new System.Drawing.Point(200, 10);
            this.buttonAllocate.Name = "buttonAllocate";
            this.buttonAllocate.Size = new System.Drawing.Size(90, 23);
            this.buttonAllocate.Text = "メモリ確保";
            this.buttonAllocate.UseVisualStyleBackColor = true;
            this.buttonAllocate.Click += new System.EventHandler(this.buttonAllocate_Click);
            //
            // buttonWrite
            //
            this.buttonWrite.Enabled = false;
            this.buttonWrite.Location = new System.Drawing.Point(296, 10);
            this.buttonWrite.Name = "buttonWrite";
            this.buttonWrite.Size = new System.Drawing.Size(90, 23);
            this.buttonWrite.Text = "書き込み";
            this.buttonWrite.UseVisualStyleBackColor = true;
            this.buttonWrite.Click += new System.EventHandler(this.buttonWrite_Click);
            //
            // buttonRead
            //
            this.buttonRead.Enabled = false;
            this.buttonRead.Location = new System.Drawing.Point(392, 10);
            this.buttonRead.Name = "buttonRead";
            this.buttonRead.Size = new System.Drawing.Size(90, 23);
            this.buttonRead.Text = "読み込み";
            this.buttonRead.UseVisualStyleBackColor = true;
            this.buttonRead.Click += new System.EventHandler(this.buttonRead_Click);
            //
            // buttonFree
            //
            this.buttonFree.Enabled = false;
            this.buttonFree.Location = new System.Drawing.Point(488, 10);
            this.buttonFree.Name = "buttonFree";
            this.buttonFree.Size = new System.Drawing.Size(90, 23);
            this.buttonFree.Text = "メモリ解放";
            this.buttonFree.UseVisualStyleBackColor = true;
            this.buttonFree.Click += new System.EventHandler(this.buttonFree_Click);
            //
            // textBoxLog
            //
            this.textBoxLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top
            | System.Windows.Forms.AnchorStyles.Bottom)
            | System.Windows.Forms.AnchorStyles.Left)
            | System.Windows.Forms.AnchorStyles.Right)));
            this.textBoxLog.Location = new System.Drawing.Point(12, 42);
            this.textBoxLog.Multiline = true;
            this.textBoxLog.Name = "textBoxLog";
            this.textBoxLog.ReadOnly = true;
            this.textBoxLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
            this.textBoxLog.Size = new System.Drawing.Size(570, 308);
            //
            // Form1
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(594, 362);
            this.Controls.Add(this.textBoxLog);
            this.Controls.Add(this.buttonFree);
            this.Controls.Add(this.buttonRead);
            this.Controls.Add(this.buttonWrite);
            this.Controls.Add(this.buttonAllocate);
            this.Controls.Add(this.labelMB);
            this.Controls.Add(this.textBoxSizeMB);
            this.Controls.Add(this.labelSize);
            this.Name = "Form1";
            this.Text = "AlignedMemory 画像バッファ デモ";
            this.ResumeLayout(false);
            this.PerformLayout();
        }

        private System.Windows.Forms.Label labelSize;
        private System.Windows.Forms.TextBox textBoxSizeMB;
        private System.Windows.Forms.Label labelMB;
        private System.Windows.Forms.Button buttonAllocate;
        private System.Windows.Forms.Button buttonWrite;
        private System.Windows.Forms.Button buttonRead;
        private System.Windows.Forms.Button buttonFree;
        private System.Windows.Forms.TextBox textBoxLog;
    }
}

おわりに

本記事では、C#で2GB以上のメモリを確保する方法として、Win32APIの_aligned_mallocをラップしたAlignedMemoryクラスを紹介しました。

ポイントをまとめると:

  • C#の標準的な方法(byte[]Marshal.AllocHGlobal)では、サイズ指定がint型のため2GB以上のメモリを確保できない
  • _aligned_mallocDllImportで呼び出すことで、ulong型のサイズ指定が可能になり、2GB以上のメモリを確保できる
  • IDisposableパターンとファイナライザを実装することで、アンマネージメモリを安全に管理できる
  • .NET 7以降ではLibraryImport属性も利用可能

今回作成したAlignedMemoryクラスにはReadByteReadInt16ReadInt32ReadInt64および対応するWriteメソッドを用意しましたが、2GB以上の大きなメモリに対して1バイトずつこれらのメソッドで読み書きするのは非効率です。実用的には、本クラスのPointerプロパティで取得したポインタをC言語のライブラリに渡して処理するか、C#でもunsafeコンテキストを使ってポインタを直接操作して読み書きするようにしてください。

// unsafeでポインタを直接操作する例
unsafe
{
    byte* p = (byte*)memory.Pointer.ToPointer();
    for (long i = 0; i < (long)memory.Size; i++)
    {
        p[i] = (byte)(i % 256);
    }
}

なお、画像処理で2GB以上の画像を扱う場合、メモリの確保だけでなく、2GB以上の画像を表示したり、保存したりすること自体も大変です。一般的な画像フォーマット(BMP、TIFF等)やGDI+のBitmapクラスにもサイズ制限があるため、独自の表示・保存処理が必要になります。

現実的には、画像データを縦方向に分割し、各分割画像のサイズが2GBを超えないようにして処理する方が現実的です。例えば、幅32,768ピクセル × 高さ32,768ピクセル × 3チャンネル(RGB)の約3GBの画像であれば、上半分と下半分に分割して、それぞれ約1.5GBの画像として個別に処理・表示・保存する方法が考えられます。この方法であれば、C#の標準的なBitmapクラスやGDI+の機能をそのまま活用でき、実装もシンプルになります。

一方で、分割せずに2GB以上の画像をそのまま扱いたい場合は、AVALDATAのC#のSDK(AcapLib2)が2GB以上の画像データを扱えるように設計されており、大容量画像の取り込み・表示・保存をサポートしています。大容量画像を扱う業務アプリケーションの開発では、このようなSDKの活用も検討してみてください。

3
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?