是否有可能返回包含多个单元格中的一个实例的单元格数组?

时间:2013-09-17 10:49:40

标签: matlab memory-management mex

我写了一些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;

有可能吗?垃圾收集器如何知道释放它? 谢谢。

4 个答案:

答案 0 :(得分: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 structure中的引用计数)

实际上这是MATLAB幕后通常会发生的事情。以此为例:

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

如您所知,MATLAB中的单元数组基本上是mxArray,其数据指针指向其他mxArray变量的数组。

在上面的例子中,MATLAB首先创建一个对应于5000x5000矩阵的mxArray。这将存储在第一个单元格c{1}中。

对于其余的单元格,MATLAB创建“轻量级”mxArray,它基本上与第一个单元格元素共享其数据,即它的数据指针指向保存巨大矩阵的同一块内存。 / p>

因此,矩阵只有一个副本,除非您修改其中一个(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))是有问题的,因为在销毁第一个单元格后,ref-counter立即达到零。因此,对于后续单元,MATLAB将尝试释放已经释放的数组,从而导致内存损坏。

答案 1 :(得分: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;
}

答案 2 :(得分: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
}

答案 3 :(得分:0)

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

您的代码无声地假设它运行的是 64 位 MATLAB 版本,并且 mwSize 是 64 位。如果这用于 32 位 MATLAB 版本并且 mwSize 为 32 位,则您计算的 ref_count 位置将不正确。

如果没有您正在使用的库函数的必要头文件,代码将无法正常工作。即,在没有原型的 C 中,返回 float 的函数将被假定为返回 int 并且计算结果最终将是错误的。也许在顶部包含这些行以明确说明:

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

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

mexCallMATLAB 创建从头开始返回的 mxArray。您不需要“预分配”结果。事实上,您所做的只是造成内存泄漏,因为指向 mxCreateNumericMatrix(etc) 调用的指针被 mexCallMATLAB 调用覆盖。解决方案只是定义返回变量而不是其他任何东西。例如,

mxArray *version;

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

mxDestroyArray(version);
mxFree(str);

mxArray 的 ref_count 字段是一个 32 位整数。它位于另一个用于位标志(isComplex、isNumeric、isSparse 等)的 32 位整数旁边。但是,您指向 ref_count 就好像它是一个 64 位整数 mwSize,然后基于此递增它。如果实际的 32 位 ref_count 恰好与 64 位 mwSize 的低位 32 位对齐,这可能会起作用,但 IMO 有点不确定,因为它似乎取决于 64 位整数的字序。您可能想要修改它以使其更加健壮。

您可能还对此处发布的 MATLAB 版本代码(编译时和运行时)感兴趣: https://www.mathworks.com/matlabcentral/fileexchange/67016-c-mex-matlab-version