能否返回一个包含一个实例分散在几个单元中的单元数组?

5

我写了一些mex函数,并需要返回大量的字符串数组。

我是这样做的:

  mxArray * array = mxCreateCellMatrix(ARRAY_LEN, 1);
  for (size_t k = 0; k < ARRAY_LEN; ++ k) {
      mxArray *str = mxCreateString("Hello");
      mxSetCell(array, k, str);
  }
  prhs[0] = array;

然而,由于该字符串始终具有相同的值,我希望只创建一个实例。

  mxArray * array = mxCreateCellMatrix(ARRAY_LEN, 1);
  mxArray *str = mxCreateString("Hello");

  for (size_t k = 0; k < ARRAY_LEN; ++ k) {
      mxSetCell(array, k, str);
  }
  prhs[0] = array;

这是否可能?垃圾收集器如何知道释放它?

感谢您。

1
@Shai:一旦你清除从MEX函数返回的变量,它就会立即导致MATLAB崩溃。单元格数组以深度递归方式逐个销毁。然而,存储的所有指针基本上都是同一个数组,因此尝试多次释放相同的内存将导致内存损坏。 - Amro
4个回答

6
您提供的第二个代码不安全,不应使用,因为它可能会导致MATLAB崩溃。相反,您应该编写:
mxArray *arr = mxCreateCellMatrix(len, 1);
mxArray *str = mxCreateString("Hello");
for(mwIndex i=0; i<len; i++) {
    mxSetCell(arr, i, mxDuplicateArray(str));
}
mxDestroyArray(str);
plhs[0] = arr;

很遗憾,这不是最有效的存储内存使用方式。想象一下,如果我们不是使用一个小字符串,而是存储一个非常大的矩阵(在单元格中复制),那么情况会怎样。


现在可以完成您最初想要的操作,但您需要采用非文档化的技巧(例如创建共享数据副本或手动增加mxArray_tag结构体中的引用计数)。

实际上,在MATLAB幕后通常会发生这种情况。 以这个为例:

>> c = cell(100,100);
>> c(:) = {rand(5000)};

如您所知,在MATLAB中,一个单元数组基本上是一个mxArray,它的数据指针指向其他mxArray变量的数组。
在上述情况下,MATLAB首先创建与5000x5000矩阵相对应的mxArray。这将存储在第一个单元中。
对于其余的单元格,MATLAB创建“轻量级”mxArray,它们基本上与第一个单元格元素共享其数据,即其数据指针指向保存巨大矩阵的同一块内存。
因此,始终只有一个矩阵的副本,除非您修改其中之一(c{2,2}(1)=99),此时MATLAB必须“取消链接”该数组并为此单元格元素制作单独的副本。
您可以看到,每个mxArray结构在内部都有一个引用计数器和交叉链接指针,以使这种数据共享成为可能。
提示:您可以通过打开format debug选项并比较各个单元格的pr指针地址来研究此数据共享行为。
相同的概念也适用于结构字段,因此当我们编写以下代码时:
x = rand(5000);
s = struct('a',x, 'b',x, 'c',x);

所有字段都将指向 x 中相同的数据副本。

编辑:

我忘记展示我提到的未记录的解决方案 :)

mex_test.cpp

#include "mex.h"

extern "C" mxArray* mxCreateReference(mxArray*);

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
    mwSize len = 10;
    mxArray *arr = mxCreateCellMatrix(len, 1);
    mxArray *str = mxCreateString("Hello");
    for(mwIndex i=0; i<len; i++) {
        // I simply replaced the call to mxDuplicateArray here
        mxSetCell(arr, i, mxCreateReference(str));
    }
    mxDestroyArray(str);
    plhs[0] = arr;
}

MATLAB

>> %c = repmat({'Hello'}, 10, 1);
>> c = mex_test()
>> c{1} = 'bye'
>> clear c
mxCreateReference函数每次被调用时都会增加str数组的内部引用计数器,告诉MATLAB它有其他副本。
因此,当您清除生成的单元格数组时,每个单元格将依次减少该计数器,直到计数器达到0,此时可以安全地销毁相应的数组。
直接使用数组(mxSetCell(arr, i, str))存在问题,因为在销毁第一个单元格后,引用计数器立即达到零。因此,对于后续单元格,MATLAB将尝试释放已经被释放的数组,导致内存损坏。

请点击此处了解有关MEX文件中共享数组的更多信息:http://www.mk.tu-berlin.de/Members/Benjamin/mex_sharedArrays - Amro
这是一篇由James Tursa撰写的精彩文章,总结了所有与MEX内存管理相关的内容,我们希望这些内容能够得到适当的文档支持和官方支持:http://www.mathworks.com/matlabcentral/answers/79046-mex-api-wish-list - Amro

3

不好的消息是...从R2014a开始(可能是R2013b但我无法检查),mxCreateReference在库中不再可用(可能是缺失或未导出),因此链接将失败。这里有一个替换函数,可以通过手动增加引用计数来插入mxArray:

struct mxArray_Tag_Partial {
    void *name_or_CrossLinkReverse;
    mxClassID ClassID;
    int VariableType;
    mxArray *CrossLink;
    size_t ndim;
    unsigned int RefCount; /* Number of sub-elements identical to this one */
};

mxArray *mxCreateReference(const mxArray *mx)
{
    struct mxArray_Tag_Partial *my = (struct mxArray_Tag_Partial *) mx;
    ++my->RefCount;
    return (mxArray *) mx;
}

我刚看到你的帖子,想澄清一下,在R2014a中确实删除了许多未记录的C函数(包括上面示例中使用的mxCreateReference)。但是幸运的是,新的等效*C++*函数已经代替它们加入。这些函数在matrix::detail::noninlined::mx_array_api命名空间下公开。请参见此处的说明以了解如何更新代码...总之,很高兴在这里见到你,詹姆斯,欢迎来到Stack Overflow :) - Amro
例如,使用Dependency Walker,我可以看到R2015a上在libmx.dll中导出了以下函数:struct mxArray_tag * matrix::detail::noninlined::mx_array_api::mxCreateReference(struct mxArray_tag const *) - Amro
不幸的是,我认为这个功能已经无法按预期工作了。根据此帖子中Bruno和James的说法,似乎在最新版本(2018-2020年)中,所有东西都变得一团糟了。https://www.mathworks.com/matlabcentral/answers/396103-mxcreateshareddatacopy-no-longer-supported-in-r2018a - Jimbo
我添加了自己的答案,考虑到2019b中发生的引用计数器位置变化。 - Jimbo

0

@Jimbo,对你发布的代码有一些评论:

你的代码假定它正在运行64位MATLAB版本,并且mwSize是64位。如果在32位MATLAB版本中使用并且mwSize是32位,则计算出的ref_count位置将不正确。

如果没有使用所需的库函数头文件,代码将无法正常工作。例如,在C语言中,如果没有原型,返回浮点数的函数将被认为返回整数,计算结果将出错。也许可以在顶部包含这些行以明确说明:

#include <stdlib.h>  /* strtof */
#include <math.h> /* roundf */

我没有看到任何逻辑,可以通过将单个数字分数“加0”来使9.9看起来比您指示的9.12小。例如,9.12只会导致minor_ver为1,而不是您所指示的12。这应该被修复。

mexCallMATLAB 从头开始创建返回的mxArray。您不需要“预分配”结果。实际上,您正在做的只会创建内存泄漏,因为mxCreateNumericMatrix(等)调用的指针被mexCallMATLAB调用覆盖。解决方案很简单,只需定义返回变量即可。例如,

mxArray *version;

你应该释放用于计算版本号的临时内存。是的,这些将在垃圾回收列表中(在R2017a之前,str不会在垃圾回收列表中),但是最好的做法是在完成后立即释放内存。例如,在计算ref_offset之后执行以下操作:

mxDestroyArray(version);
mxFree(str);

mxArray的ref_count字段是一个32位整数。它旁边还有另一个32位整数,用于位标志(isComplex、isNumeric、isSparse等)。然而,您正在将ref_count指向64位整数mwSize,并根据此递增它。如果实际的32位ref_count恰好与64位mwSize的低32位对齐,那么这可能有效,但在我看来这有点不确定,因为它似乎取决于64位整数的字序。您可能需要修改此内容以使其更加健壮。
您可能还对发布在此处的MATLAB版本代码(编译时和运行时)感兴趣: https://www.mathworks.com/matlabcentral/fileexchange/67016-c-mex-matlab-version

谢谢James!当我尝试使用别人的代码时,不显示包含语句是我的一个小习惯--我的错。大多数评论都有道理。不确定如何处理32/64位MATLAB(两个指针在前面,所以32位时减去2?)-但现在忽略32位也没问题。在次要版本中肯定有一个错误,并且幸运地得到了引用计数和较低位切换的帮助--我想知道为什么计数没有意义但仍然有效! - Jimbo

0
在2019b中,引用计数器的位置发生了变化。为了解决这个问题,我现在在运行时检测MATLAB版本,并相应地更改头文件中的偏移量。也可以进行编译时检查,但我希望我的mex文件能够跨版本工作而无需重新编译。请注意,由于我不再显式访问结构体,因此我不再具有部分结构定义。我还向用户公开了一个标志选项ALLOW_REF_COUNT,以便在编译时执行深度复制。欢迎反馈/建议...
#include "stdlib.h"  /* atoi */
#include "string.h" /* strchr */
int ref_offset = -1;

mxArray* mxCreateReference(const mxArray *mx){
    #ifdef ALLOW_REF_COUNT
        if (ref_offset == -1){
            //Grabs output of version() e.g. 9.9.0.15 etc.
            //and translates into 909 - we add a 0 because we would want
            //9.12 to be 912 and newer/higher than 9.9
            mxArray *version;
            mexCallMATLAB(1,&version,0, NULL, "version");
            char *str = mxArrayToString(version);
            char* loc = strchr(str, '.');
            int mantissa = atoi(loc+1);
            int whole = atoi(str);
            int version_id = whole*100 + mantissa;

            mxDestroyArray(version);
            mxFree(str);
            
            //_Static_assert => c11
            _Static_assert(sizeof(void *) == 8, "Error: 32bit MATLAB not supported");
            
            //907 -> 2019b
            if (version_id < 907){
                ref_offset = 8;
            }else{
                ref_offset = 6;
            }
        }

        uint32_t *ref_count = ((uint32_t *) mx) + ref_offset; 
        (*ref_count)++;

        //struct mxArray_Tag_Partial *my = (struct mxArray_Tag_Partial *) mx;
        //++my->RefCount;
        return (mxArray *) mx;
    #else
        return mxDuplicateArray(mx);
    #endif
}

我的回复太长了,无法放在评论中,请查看我的答案。 - James Tursa

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