我正在使用C#进行流体模拟。每个循环我需要计算空间中离散点处流体的速度。作为计算的一部分,我需要几十千字节的空间来容纳一些double []数组(数组的确切大小取决于一些输入数据)。数组仅在使用它们的方法的持续时间内需要,并且有一些不同的方法需要这样的临时空间。
我认为,构建临时数组有几种不同的解决方案:
每次调用方法时,使用'new'从堆中获取内存。这就是我最初做的事情,但它给垃圾收集器带来了很大的压力,而每秒一到两次的几毫秒尖峰真的很烦人。
在调用方法时,将临时数组作为参数传递。问题是,这迫使用户管理它们,包括适当地调整它们,这是一个巨大的痛苦。而且由于它改变了API,因此难以使用或多或少的临时存储器。
在不安全的上下文中使用stackalloc从程序堆栈中分配临时内存。这样可以正常工作,除了我需要使用/ unsafe进行编译并在我的代码中不断地使用不安全的块,我想避免这些。
程序启动时预先分配私有数组。这很好,除非我不一定知道我需要的数组的大小,直到我可以查看一些输入数据。它变得非常混乱,因为你不能将这些私有变量的范围限制为单一方法,因此它们不断地污染命名空间。随着需要临时存储器的方法数量的增加,它的扩展性也很差,因为我分配的内存很多,只占用了一小部分时间。
创建某种中央池,并从池中分配临时内存阵列。这个问题的主要问题是我没有看到从中央池分配动态大小的数组的简单方法。我可以使用起始偏移和长度,并且所有临时内存基本上共享一个大型数组,但我有很多现有的代码假定double [] s。而且我必须小心这样一个池线程安全。
...
有没有人有类似问题的经验?从经验中提供的任何建议/课程?
答案 0 :(得分:7)
我同情你的情况;当我在Roslyn工作时,我们非常仔细地考虑了分配临时工作阵列的收集压力所带来的潜在性能问题。我们解决的解决方案是汇集策略。
在编译器中,数组大小往往很小,因此经常重复。在您的情况下,如果您有大型阵列,那么我要做的就是遵循Tom的建议:简化管理问题并浪费一些空间。当您向池请求x大小的数组时,将x舍入到最接近的2的幂并分配该大小的数组,或者从池中取一个。调用者得到一个有点太大的数组,但是可以编写它们来处理它。在池中搜索适当大小的数组应该不会太难。或者你可以维护一堆池,一个池用于大小为1024的数组,一个用于2048,依此类推。
编写线程安全池并不太难,或者您可以使池线程保持静态并为每个线程创建一个池。
棘手的一点是在池中重新获得内存。有几种方法可以解决这个问题。首先,你可以简单地要求池化内存的用户在完成数组后调用“回到池中”方法,如果他们不想承担收集压力的费用。
另一种方法是在数组周围编写一个外观包装器,使其实现IDisposable,以便您可以使用“using”(*),并在其上创建一个终结器,将对象放回池中,然后重新生成它。 (确保让终结者回到“我需要最终确定”位。)复活的终结者让我感到紧张;我个人更喜欢前一种方法,这就是我们在罗斯林所做的。
(*)是的,这违反了“使用”应表明非托管资源正在返回操作系统的原则。基本上我们通过自己的管理将托管内存视为非托管资源,所以它并没有那么糟糕。
答案 1 :(得分:3)
你可以在这样的using语句中包含使用这些临时数组的代码:
using(double[] scratchArray = new double[buffer])
{
// Code here...
}
这将通过在using语句末尾调用descructor来显式释放内存。
它看起来像这样:(使用来自Algorithm for finding the smallest power of two that's greater or equal to a given value的pow2roundup)
private static Dictionary<int,double[]> scratchArrays = new Dictionary<int,double[]>();
/// Round up to next higher power of 2 (return x if it's already a power of 2).
public static int Pow2RoundUp (int x)
{
if (x < 0)
return 0;
--x;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x+1;
}
private static double[] GetScratchArray(int size)
{
int pow2 = Pow2RoundUp(size);
if (!scratchArrays.ContainsKey(pow2))
{
scratchArrays.Add(pow2, new double[pow2]);
}
return scratchArrays[pow2];
}
编辑:线程安全版本: 这仍然会有垃圾收集的东西,但它将是特定于线程的,并且应该减少开销。
[ThreadStatic]
private static Dictionary<int,double[]> _scratchArrays;
private static Dictionary<int,double[]> scratchArrays
{
get
{
if (_scratchArrays == null)
{
_scratchArrays = new Dictionary<int,double[]>();
}
return _scratchArrays;
}
}
/// Round up to next higher power of 2 (return x if it's already a power of 2).
public static int Pow2RoundUp (int x)
{
if (x < 0)
return 0;
--x;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x+1;
}
private static double[] GetScratchArray(int size)
{
int pow2 = Pow2RoundUp(size);
if (!scratchArrays.ContainsKey(pow2))
{
scratchArrays.Add(pow2, new double[pow2]);
}
return scratchArrays[pow2];
}