【发布时间】:2022-02-21 01:45:15
【问题描述】:
我继承了 .NET 框架 C#/非托管 C++ 项目的维护,并被要求尝试改进它的性能。在追求低调的果实和一些分析之后,我可以轻松做的下一件事似乎是试图降低跨 C#/C++ 互操作边界传输大型缓冲区的成本,因为程序花费了大约 30% 的时间来做这个编组/复制。我认为这很简单,因为 C# 具有良好的互操作支持,并且文档似乎表明这是可能的。
在尝试了几种方法但都失败后,我制作了一个测试解决方案来简化事情并确保我理解正在发生的事情。从原始代码库中删除抽象后,该示例的 C# 代码如下所示:
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Diagnostics;
namespace InteropExample
{
internal class Program
{
static void Main()
{
UnsafeNativeMethods.InitCpp();
IntPtr buf = Marshal.AllocHGlobal(4096);
int size = 0;
UnsafeNativeMethods.OnGetDataBuffer(buf, ref size);
Debug.WriteLine("C# buffer in:\t{0:X}", buf);
Marshal.FreeHGlobal(buf);
}
// docs say we should see "substantial performance savings" with this attribute
[SuppressUnmanagedCodeSecurity]
internal static class UnsafeNativeMethods
{
[DllImport("CppSide.dll")]
public static extern void InitCpp();
[DllImport("CppSide.dll")]
public static extern void OnGetDataBuffer(IntPtr buffer, ref int size);
}
}
}
C++ 方面,我对原始代码库中的各个部分的注释和抽象被删除:
#include "CppSide.h"
#include <memory>
// way more complicated, former owner implemented their own shared_ptr
// and Memorypool system and has a ton of getter, setter, and helper functions for it
class Buffer
{
public:
size_t _size;
size_t _usedSize;
char* _data;
Buffer() :
_size(4096),
_data(NULL)
{
size_t charSize = _size / sizeof(char);
// guessing this is to get around destructors running
// and get better SSE2 vectorization
_data = (char*)_aligned_malloc(_size, (size_t)256);
// used size is set by the user after it has requested a buffer from the buffer pool
// for now we set it to an arbitrary value
_usedSize = 768;
}
~Buffer() {
_aligned_free(_data);
}
};
std::shared_ptr<Buffer> out;
extern "C" __declspec (dllexport) void InitCpp()
{
// in real program this starts up various threads and pipelines
// that produce and accept the buffers, for now we will simulate this
// just by creating one buffer heading out
out = std::make_shared<Buffer>();
}
extern "C" __declspec (dllexport) void OnGetDataBuffer(char* buffer, int size) {
memcpy(buffer, out->_data, out->_usedSize);
size = out->_usedSize;
std::ostringstream ss;
ss << "C++ out Buffer ptr:\t" << std::hex << static_cast<void*>(buffer) << std::endl;
OutputDebugStringA(ss.str().c_str());
out = NULL;
// The actual code doesn't do remove out explicitly
// but the buffer is returned to the pool soon after
// being called by the pipeline replacing it
}
当我尝试删除 memcpy 和 HGlobalAlloc 时,将 _data 指针直接传递到 C# 端,并固定共享指针以使缓冲区不会被删除,我将 IntPtr 设置为零回到 C# 端。我认为这可能是我分配方式的问题,HGlobalAlloc 的底层分配器是GlobalAlloc,所以我还将缓冲区中的_aligned_malloc 和_aligned_free 调用与GlobalAlloc(GPTR, charSize) 和GlobalFree 交换,但仍然得到了相同的结果。这给我留下了当前无效的例子:
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Diagnostics;
namespace InteropExample
{
internal class Program
{
static void Main()
{
UnsafeNativeMethods.InitCpp();
IntPtr buf = IntPtr.Zero;
int size = 0;
UnsafeNativeMethods.OnGetDataBuffer(ref buf, ref size);
Debug.WriteLine("C# buffer in:\t{0:X}", buf);
}
[SuppressUnmanagedCodeSecurity]
internal static class UnsafeNativeMethods
{
[DllImport("CppSide.dll")]
public static extern void InitCpp();
[DllImport("CppSide.dll")]
public static extern void OnGetDataBuffer(ref IntPtr buffer, ref int size);
}
}
}
#include "CppSide.h"
#include <memory>
#include <windows.h>
#include <iostream>
#include <sstream>
class Buffer
{
public:
size_t _size;
size_t _usedSize;
char* _data;
Buffer() :
_size(4096),
_data(NULL)
{
size_t charSize = _size / sizeof(char);
_data = (char *)GlobalAlloc(GPTR, charSize);
// used size is set by the user after it has requested a buffer from the buffer pool
// for now we set it to an arbitrary value
_usedSize = 768;
}
~Buffer() {
GlobalFree(_data);
}
};
std::shared_ptr<Buffer> out;
extern "C" __declspec (dllexport) void InitCpp()
{
out = std::make_shared<Buffer>();
}
extern "C" __declspec (dllexport) void OnGetDataBuffer(char* buffer, int size) {
buffer = out->_data;
size = out->_usedSize;
std::ostringstream ss;
ss << "C++ out Buffer ptr:\t" << std::hex << static_cast<void*>(buffer) << std::endl;
OutputDebugStringA(ss.str().c_str());
}
再次查看 C# 互操作文档后,我找不到能够将指针传回非托管内存的示例,所以我想我的问题如下:
- 是否可以将指向从 C++ 分配的非托管内存的指针传回 C# 以避免编组/复制大型缓冲区?
- 如果必须在托管端分配缓冲区,然后将非托管缓冲区的内容复制到其中,是否有更有效的方法?我可以使用哪些池化策略?我查看了MemoryPool,但我不明白它是否/如何用于替换原始代码中的
Marshal.AllocHGlobal调用。
【问题讨论】:
-
~Buffer() {GlobalFree(_data);}-- 如果 C# 应该管理内存,那么它不应该存在。如果那个对象在你不知情的情况下被破坏了,你的缓冲区就会变成一团烟雾。这应该很简单——GlobalAlloc返回一个 HGLOBAL,您应该能够将HGLOBAL返回到 C#。如果有任何疑问,请查看 C# 的 pinvoke 以及对 GlobalAlloc、GlobalFree 等的调用。 -
我知道析构函数问题,我将不得不解决它,可能通过以某种方式固定缓冲区并与缓冲区一起传递回调以释放它。不过一次一个问题。我试图在我的非工作示例中使用
GlobalAlloc,但没有影响。我在选择标志时遵循了GlobalAlloc文档中的建议,演员阵容是否会导致问题? -
在此处查看您的
OnGetDataBuffer函数:buffer = out->_data;-- 这并没有达到您的预期目的。传递的char *是按值传递的,因此您对buffer所做的任何更改都是本地的。最好返回句柄,在查看您的代码之后,应该从一开始就意识到这一点。 -
void foo(int *ptr) { ptr = new int; } int main() { int *p = nullptr; foo(p); }你会注意到在调用foo之后p仍然是nullptr,并且没有设置为new int;的值。简而言之,这就是您当前代码的问题 -
啊,我明白了。感谢您的帮助,我真的需要更多地离开 Java 领域并使用引用和指针,所以我不会一直忘记这些简单的事情,比如按值传递与按引用传递。我在两边打印的十六进制不匹配,但我认为这是由于我缺少格式的差异,因为我能够看到通过缓冲区传递的数据。再次感谢您的帮助。
标签: c# c++ visual-c++