如何在C#中分配超过MaxInteger字节的内存

3

我希望能够分配超过MaxInteger字节的内存。

Marshall.AllocHGlobal()期望一个整数 - 所以我不能使用它。还有其他方法吗?

更新

我将平台更改为x64,然后运行了下面的代码。

myp似乎具有正确的长度:约3.0G。但是,"buffer" 顽固地最大化到2.1G。

有什么想法吗?

    var fileStream = new FileStream(
          "C:\\big.BC2",
          FileMode.Open,
          FileAccess.Read,
          FileShare.Read,
          16 * 1024,
          FileOptions.SequentialScan);
    Int64 length = fileStream.Length;
    Console.WriteLine(length);
    Console.WriteLine(Int64.MaxValue);
    IntPtr myp = new IntPtr(length);
    //IntPtr buffer = Marshal.AllocHGlobal(myp);
    IntPtr buffer = VirtualAllocEx(
        Process.GetCurrentProcess().Handle,
        IntPtr.Zero,
        new IntPtr(length),
        AllocationType.Commit | AllocationType.Reserve,
        MemoryProtection.ReadWrite);
    unsafe
    {
        byte* pBytes = (byte*)myp.ToPointer();
        var memoryStream = new UnmanagedMemoryStream(pBytes, (long)length, (long)length, FileAccess.ReadWrite);
        fileStream.CopyTo(memoryStream);

12
为什么你想这样做? - Lasse V. Karlsen
Vitor,我的测试代码在以下情况下失败: IntPtr myp = new IntPtr(length); //length = 3 000 000 000 因此,似乎它在我的64位机器上并不大... - ManInMoon
Chris,在我的平台构建中只有Active(x86)作为选项...你认为这仍然会受到Hans所说的2G限制的约束吗? - ManInMoon
我不知道答案,但我建议你尝试另一种方法来实现你想要达到的目标。在我看来,你应该考虑实现某种数据结构来处理你的数据,而不是分配巨大的缓冲区。当然,这样做更加复杂,但有时在进行非平凡数据处理时是不可避免的。 - tia
1
如果您无法将平台设置为64位版本,则无法获得超过2GB的内存。 - Gabe
显示剩余6条评论
6个回答

7
这在当前主流硬件上是不可能的。即使在64位机器上,内存缓冲区也受到2GB的限制。对缓冲区的索引寻址仍然使用32位有符号偏移量。从技术上讲,可以生成能够索引更多内容的机器代码,使用一个寄存器来存储偏移量,但这样做是昂贵的,会减慢所有数组索引的速度,即使那些没有超过2GB的数组也是如此。
此外,您无法从32位进程可用的地址空间中获取大于约650MB的缓冲区。由于虚拟内存包含各种地址处的代码和数据,因此没有足够的连续内存页可用。
像IBM和Sun这样的公司出售可以做到这一点的硬件设备。

1
你确定吗?我现在身边没有超过2GB的机器,所以无法测试,但是我能找到的所有信息都指向VirtualAlloc()能够分配超过2GB的内存。至于实际寻址缓冲区,那是一个单独的问题,但如果可以为内存映射文件完成,那么对于普通内存也肯定可以完成。 - Rasmus Faber
1
有趣...刚刚阅读了英特尔手册,确实位移和立即操作数在x64上都有最大32位。 - wj32
@Hans Passant:那个链接实际上与您的观点相矛盾。引用:“使用本地分配。您总是可以P/Invoke到NT的本地堆中并分配内存,然后使用不安全的代码来访问它。[...]分配8GB块[...]”。 - Rasmus Faber
@Rasmus:这不是关于分配的问题,而是在你得到数组后对其进行索引的问题。这是我回答中的“在技术上可能”的条款。 - Hans Passant
@Hans:所以你使用指针增量而不是索引增量进行迭代。没什么大不了的。大多数优化编译器都会为您执行此转换。 - Ben Voigt
这是一个数组,不是一个列表。 - Hans Passant

4
我参与了您之前提出的另一个问题,并且我真诚地认为您在这里正在进行一场徒劳的战斗。您需要可能探索其他处理数据的方法,而不是将所有内容读入内存中。
如果我理解正确,您有多个线程同时处理数据,这就是为什么您不想直接从文件中处理,因为我认为这会导致I/O争用。
您是否考虑过或者是否有可能读取一块数据到内存中,让线程处理该块,然后读取下一块或者线程处理下一块?这样,在任何时候,您都不会将超过一块的数据保存在内存中,但是所有线程都可以访问该块。虽然这并不是最优解,但我认为这是一个起点。如果这是可行的,那么可以探索优化此方案的选项。
更新:使用平台调用分配非托管内存并从.NET中使用它的示例。
既然您如此确定需要将这么多数据加载到内存中,我想编写一个小的测试应用程序来验证这将起作用。为此,您将需要以下内容。
  1. 使用/unsafe编译选项进行编译。
  2. 如果您想要分配超过2GB的内存,还需要将目标平台切换为x64

*上面第2点有些复杂,在64位操作系统上,您仍然可以针对x86平台并获得完整的4 GB内存访问权限。这将需要您使用类似EDITBIN.EXE的工具,在PE头中设置LargeAddressAware标志。

这段代码使用 VirtualAllocEx 来分配非托管内存,并使用 .NET 流的隐喻UnmanagedMemoryStream 来访问非托管内存。请注意,这段代码只经过一些非常基本的快速测试,在目标64位环境下进行,并且只使用了4GB RAM。最重要的是,该进程的内存利用率只到达了约2.6GB。
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.ComponentModel;

namespace MemoryMappedFileTests
{
  class Program
  {
    static void Main(string[] args)
    {
      IntPtr ptr = IntPtr.Zero;
      try
      {
        // Allocate and Commit the memory directly.
        ptr = VirtualAllocEx(
          Process.GetCurrentProcess().Handle, 
          IntPtr.Zero, 
          new IntPtr(0xD0000000L), 
          AllocationType.Commit | AllocationType.Reserve, 
          MemoryProtection.ReadWrite);
        if (ptr == IntPtr.Zero)
        {
          throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        // Query some information about the allocation, used for testing.
        MEMORY_BASIC_INFORMATION mbi = new MEMORY_BASIC_INFORMATION();
        IntPtr result = VirtualQueryEx(
          Process.GetCurrentProcess().Handle, 
          ptr, 
          out mbi, 
          new IntPtr(Marshal.SizeOf(mbi)));
        if (result == IntPtr.Zero)
        {
          throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        // Use unsafe code to get a pointer to the unmanaged memory. 
        // This requires compiling with /unsafe option.
        unsafe
        {
          // Pointer to the allocated memory
          byte* pBytes = (byte*)ptr.ToPointer();

          // Create Read/Write stream to access the memory.
          UnmanagedMemoryStream stm = new UnmanagedMemoryStream(
            pBytes, 
            mbi.RegionSize.ToInt64(), 
            mbi.RegionSize.ToInt64(), 
            FileAccess.ReadWrite);

          // Create a StreamWriter to write to the unmanaged memory.
          StreamWriter sw = new StreamWriter(stm);
          sw.Write("Everything seems to be working!\r\n");
          sw.Flush();

          // Reset the stream position and create a reader to check that the 
          // data was written correctly.
          stm.Position = 0;
          StreamReader rd = new StreamReader(stm);
          Console.WriteLine(rd.ReadLine());
        }
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.ToString());
      }
      finally
      {
        if (ptr != IntPtr.Zero)
        {
          VirtualFreeEx(
            Process.GetCurrentProcess().Handle, 
            ptr, 
            IntPtr.Zero, 
            FreeType.Release);
        }
      }

      Console.ReadKey();
    }

    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern IntPtr VirtualAllocEx(
      IntPtr hProcess, 
      IntPtr lpAddress,
      IntPtr dwSize, 
      AllocationType dwAllocationType, 
      MemoryProtection flProtect);

    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern bool VirtualFreeEx(
      IntPtr hProcess, 
      IntPtr lpAddress, 
      IntPtr dwSize, 
      FreeType dwFreeType);

    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern IntPtr VirtualQueryEx(
      IntPtr hProcess, 
      IntPtr lpAddress, 
      out MEMORY_BASIC_INFORMATION lpBuffer, 
      IntPtr dwLength);

    [StructLayout(LayoutKind.Sequential)]
    public struct MEMORY_BASIC_INFORMATION
    {
      public IntPtr BaseAddress;
      public IntPtr AllocationBase;
      public int AllocationProtect;
      public IntPtr RegionSize;
      public int State;
      public int Protect;
      public int Type;
    }

    [Flags]
    public enum AllocationType
    {
      Commit = 0x1000,
      Reserve = 0x2000,
      Decommit = 0x4000,
      Release = 0x8000,
      Reset = 0x80000,
      Physical = 0x400000,
      TopDown = 0x100000,
      WriteWatch = 0x200000,
      LargePages = 0x20000000
    }

    [Flags]
    public enum MemoryProtection
    {
      Execute = 0x10,
      ExecuteRead = 0x20,
      ExecuteReadWrite = 0x40,
      ExecuteWriteCopy = 0x80,
      NoAccess = 0x01,
      ReadOnly = 0x02,
      ReadWrite = 0x04,
      WriteCopy = 0x08,
      GuardModifierflag = 0x100,
      NoCacheModifierflag = 0x200,
      WriteCombineModifierflag = 0x400
    }

    [Flags]
    public enum FreeType
    {
      Decommit = 0x4000,
      Release = 0x8000
    }
  }
}

谢谢Chris,但我已经知道如何通过读取2D字节数组来实现这一点 - 因此它仍然全部在内存中。并且需要分别处理每个字节数组。但这非常麻烦,我不得不做一些巧妙的技巧才能让它工作。考虑到我在这台服务器上有32G的内存 - 我有点生气我不能直接使用它... - ManInMoon
嗨,Chris,谢谢你的代码。我不得不“回答”以展示我的测试代码。你介意看一下吗? - ManInMoon

1

谢谢Rasmus - 请看下面Hans的评论 - 真遗憾! - ManInMoon

0

来自评论:

我该如何创建第二个二进制读取器,以便可以独立地读取相同的内存流?

var fileStream = new FileStream("C:\\big.BC2",
      FileMode.Open,
      FileAccess.Read,
      FileShare.Read,
      16 * 1024,
      FileOptions.SequentialScan);
    Int64 length = fileStream.Length;
    IntPtr buffer = Marshal.AllocHGlobal(length);
    unsafe
    {
        byte* pBytes = (byte*)myp.ToPointer(); 
        var memoryStream = new UnmanagedMemoryStream(pBytes, (long)length, (long)length, FileAccess.ReadWrite);
        var binaryReader = new BinaryReader(memoryStream);
        fileStream.CopyTo(memoryStream);
        memoryStream.Seek(0, SeekOrigin.Begin);
        // Create a second UnmanagedMemoryStream on the _same_ memory buffer
        var memoryStream2 = new UnmanagedMemoryStream(pBytes, (long)length, (long)length, FileAccess.Read);
        var binaryReader2 = new BinaryReader(memoryStream);
     }

0

如果您无法直接使其按照所需方式工作,请创建一个类来提供所需行为类型。因此,要使用大数组:

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace BigBuffer
{
  class Storage
  {
    public Storage (string filename)
    {
      m_buffers = new SortedDictionary<int, byte []> ();
      m_file = new FileStream (filename, FileMode.Open, FileAccess.Read, FileShare.Read);
    }

    public byte [] GetBuffer (long address)
    {
      int
        key = GetPageIndex (address);

      byte []
        buffer;

      if (!m_buffers.TryGetValue (key, out buffer))
      {
        System.Diagnostics.Trace.WriteLine ("Allocating a new array at " + key);
        buffer = new byte [1 << 24];
        m_buffers [key] = buffer;

        m_file.Seek (address, SeekOrigin.Begin);
        m_file.Read (buffer, 0, buffer.Length);
      }

      return buffer;
    }

    public void FillBuffer (byte [] destination_buffer, int offset, int count, long position)
    {
      do
      {
        byte []
          source_buffer = GetBuffer (position);

        int
          start = GetPageOffset (position),
          length = Math.Min (count, (1 << 24) - start);

        Array.Copy (source_buffer, start, destination_buffer, offset, length);

        position += length;
        offset += length;
        count -= length;
      } while (count > 0);
    }

    public int GetPageIndex (long address)
    {
      return (int) (address >> 24);
    }

    public int GetPageOffset (long address)
    {
      return (int) (address & ((1 << 24) - 1));
    }

    public long Length
    {
      get { return m_file.Length; }
    }

    public int PageSize
    {
      get { return 1 << 24; }
    }

    FileStream
      m_file;

    SortedDictionary<int, byte []>
      m_buffers;
  }

  class BigStream : Stream
  {
    public BigStream (Storage source)
    {
      m_source = source;
      m_position = 0;
    }

    public override bool CanRead
    {
      get { return true; }
    }

    public override bool CanSeek
    {
      get { return true; }
    }

    public override bool CanTimeout
    {
      get { return false; }
    }

    public override bool CanWrite
    {
      get { return false; }
    }

    public override long Length
    {
      get { return m_source.Length; }
    }

    public override long Position
    {
      get { return m_position; }
      set { m_position = value; }
    }

    public override void Flush ()
    {
    }

    public override long Seek (long offset, SeekOrigin origin)
    {
      switch (origin)
      {
      case SeekOrigin.Begin:
        m_position = offset;
        break;

      case SeekOrigin.Current:
        m_position += offset;
        break;

      case SeekOrigin.End:
        m_position = Length + offset;
        break;
      }

      return m_position;
    }

    public override void SetLength (long value)
    {
    }

    public override int Read (byte [] buffer, int offset, int count)
    {
      int
        bytes_read = (int) (m_position + count > Length ? Length - m_position : count);

      m_source.FillBuffer (buffer, offset, bytes_read, m_position);

      m_position += bytes_read;
      return bytes_read;
    }

    public override void  Write(byte[] buffer, int offset, int count)
    {
    }

    Storage
      m_source;

    long
      m_position;
  }

  class IntBigArray
  {
    public IntBigArray (Storage storage)
    {
      m_storage = storage;
      m_current_page = -1;
    }

    public int this [long index]
    {
      get
      {
        int
          value = 0;

        index <<= 2;

        for (int offset = 0 ; offset < 32 ; offset += 8, ++index)
        {
          int
            page = m_storage.GetPageIndex (index);

          if (page != m_current_page)
          {
            m_current_page = page;
            m_array = m_storage.GetBuffer (m_current_page);
          }

          value |= (int) m_array [m_storage.GetPageOffset (index)] << offset;
        }

        return value;
      }
    }

    Storage
      m_storage;

    int
      m_current_page;

    byte []
      m_array;
  }

  class Program
  {
    static void Main (string [] args)
    {
      Storage
        storage = new Storage (@"<some file>");

      BigStream
        stream = new BigStream (storage);

      StreamReader
        reader = new StreamReader (stream);

      string
        line = reader.ReadLine ();

      IntBigArray
        array = new IntBigArray (storage);

      int
        value = array [0];

      BinaryReader
        binary = new BinaryReader (stream);

      binary.BaseStream.Seek (0, SeekOrigin.Begin);

      int
        another_value = binary.ReadInt32 ();
    }
  }
}

我将问题分成了三个类:

  • Storage - 存储实际数据的地方,使用分页系统
  • BigStream - 使用 Storage 类作为其数据源的流类
  • IntBigArray - 包装 Storage 类型,提供 int 数组接口

以上内容可以得到显著改进,但它应该能够给你解决问题的思路。


谢谢Skizz - 感激你分享这个。 - ManInMoon

0

如果没有使用pinvoke调用,从托管代码中实现这一点是不可能的,而且有很好的理由。分配那么多内存通常是一个需要重新审视的糟糕解决方案的迹象。

您能告诉我们为什么您认为需要这么多内存吗?


2
你仍然不应该这样做,文件应该分段读取。 - Lasse V. Karlsen
1
这是一个漫长的故事!!!如果你有兴趣,请看看我的其他问题——它们都是关于同一个主题的。你会发现我在那里详细说明了原因。非常感谢。 - ManInMoon
JaredPar,你能告诉我你想到的Pinvoke调用吗? - ManInMoon
这应该是对问题的评论,而不是答案。 - Robear

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接