如何实现简单的线程安全内存管理,防止碎片
本文关键字:管理 内存 安全 碎片 线程 何实现 实现 简单 | 更新日期: 2023-09-27 18:31:17
我需要实现简单的线程安全内存管理,防止碎片化。我已经阅读了一些关于这个和这个的文章,但我无法弄清楚如何在 C# 中启动实现。
要点:95% 的内存分配请求将是小于 1K 的块!
有人可以给我一些代码开始吗?
编辑我写了一个Allocator
但我如何在Alloc
方法中不使用池。我如何更改它以便它使用池?
class Allocator
{
private int _id;
//TODO: it must be struct!
class Block
{
public int offset;
public int blockLength;
}
private readonly Dictionary<int, Block> idToBlock = new Dictionary<int, Block>();
private List<byte> rawData;
private int allocatedBlocksLength;
// sync
private readonly object _sync = new object();
public int Alloc(int count)
{
rawData.AddRange(new byte[count]);
idToBlock.Add(_id, new Block { offset = allocatedBlocksLength, blockLength = count });
var blockId = _id;
++_id;
allocatedBlocksLength += count;
return blockId;
}
public void Free(int id)
{
// Search in table
Block block;
if (!idToBlock.TryGetValue(id, out block))
return;
// remove object and update all offsets that after our block
foreach (var kv in idToBlock)
{
if (kv.Key == id)
continue;
if (kv.Value.offset > block.offset)
continue;
// changing indexes
kv.Value.offset -= block.blockLength;
}
// update how much left
allocatedBlocksLength -= block.blockLength;
}
}
如果确实需要 .NET 应用程序的自定义内存管理器,则不应遵循非托管环境(第二个链接)中的提示(或简单地翻译代码)。
.NET 环境中的内存分配完全不同,内存碎片化得更多(因为默认分配器特权分配速度),但它可以压缩(因此内存碎片问题并不是真正的问题)。
这不是您的情况,但大型对象(现在此阈值设置为 85 KB)将使用不同的策略进行分配,并且它们不会被压缩。我认为只有当您创建大量短命的大对象时,您可能才需要自定义分配器。
第一个链接提供了一个非常幼稚的实现,您是否在多线程环境中对其进行了分析?在您的情况下,您确定它的性能优于默认分配吗?即使它的性能好一点,你确定你需要它吗?
为了使内存分配器线程安全,您可以为每个线程使用不同的堆,或者只是锁定数据结构(例如,如果您将可用内存块列表保留在 LinkedList 中,则可以在从列表中删除节点时锁定结构)。这不是一个可以用几行来解释的主题,如果你真的对这些内部感兴趣,你可以阅读伟大的"通过 C# 的 CLR"一书。
当对象分配非常广泛时,您可以对对象使用复活机制,但这会增加许多必须评估的复杂性,通常您要付出的代价更大。您可以从工厂方法开始,例如:
MyObject obj = ObjectFactory.Allocate();
而不是简单的:
MyObject obj = new MyObject();
通过这种方式,如果您真的需要它,您可以切换到其他东西,但是......
...一个小提示:如果您不确定自己在做什么并且在您分析了当前的内存分配策略之后,请不要玩内存分配。
(我很想为此消息使用更大的字体)
这可能是您可以对应用程序执行的最糟糕的事情之一,因为会使它变慢,并且代码的可读性会降低。 99.999% 的应用程序不需要这些自定义内容,您确定您的应用程序需要吗?
编辑
从示例中,您并不清楚您在做什么。您的 Alloc 方法返回一个 ID,但如何获取分配的数据?无论如何。。。
如果你真的需要做这样的事情...
- 不要保留字节列表,你只会浪费内存。
- 不要提供
Free
方法,你使用的是 .NET,因此请依赖 GC。 - 保留可用块(
Block
对象)的列表。在Allocate
方法中,您将在可用块列表中搜索所需大小的块。如果找到它,则返回该块并将其从列表中删除。如果您没有找到该块,则必须分配它并将其简单地返回给调用方。 - 在
Block
对象的终结器中调用 GC。ReRegisterForFinalize 方法,并将对象插入到可用块列表中。
非常简单的实现,作为示例,考虑不是一个真正的程序:
sealed class Block
{
internal Block(int size)
{
Data = new byte[size];
}
~Block()
{
BlockFactory.Free(this);
GC.ReRegisterForFinalize(this);
}
public byte[] Data
{
get;
private set;
}
}
static class BlockFactory
{
public static Block Allocate(int size)
{
lock (_freeBlocks)
{
foreach (Block block in _freeBlocks)
{
if (block.Data.Length == size)
{
_freeBlocks.Remove(block);
return block;
}
}
return new Block(size);
}
}
internal static void Free(Block block)
{
lock (_freeBlocks) _freeBlocks.Add(block);
}
private static List<Block> _freeBlocks = new List<Block>();
}
请注意:
- 这种实现根本没有效率(在这种情况下,更好的解决方案可能是
ReadWriterLockSlim
而不是lock
或另一种更合适的数据结构而不是List<T>
)。 - 使用枚举的搜索很糟糕,但这里只是为了清楚起见。
- 向每个对象添加终结器可能会降低性能。
- 该示例使用
Block
作为所需数据(字节数组)的容器。这是你需要的吗?
也就是说,我仍然认为在你花任何时间之前,你应该检查你是否需要它。您的应用程序是否遇到此问题?是问题所在吗?例如,假设您有一个数据处理应用程序。管道由以下阶段组成:
- 采集(以某种方式定时以定期获取数据)。
- 处理(各种过滤器)。
- 可视化。
如果为每个数据包分配一个新的缓冲区,则可能会创建许多小对象。我真的不认为这可能是一个问题,但您可以考虑在采集阶段重用相同的(预先分配的)缓冲区,而不是试图增加所有应用程序的复杂性。
我希望我的意思很清楚。