【问题标题】:Efficiency of repeated memory allocations in glibcglibc中重复内存分配的效率
【发布时间】:2010-11-01 00:32:11
【问题描述】:

下面是我对来自著名 LAPACK 数值库的 Fortran ZHEEVR 例程的 C 包装器:

void zheevr(char jobz, char range, char uplo, int n, doublecomplex* a, int lda, double vl, double vu, int il, int iu, double abstol, double* w, doublecomplex* z, int ldz, int* info)
{
    int m;
    int lwork = -1;
    int liwork = -1;
    int lrwork = -1;
    int* isuppz = alloc_memory(sizeof(int) * 2 * n);
    zheevr_(&jobz, &range, &uplo, &n, a, &lda, &vl, &vu, &il, &iu, &abstol, &m, w, z, &ldz, isuppz, small_work_doublecomplex, &lwork, small_work_double, &lrwork, small_work_int, &liwork, &info);
    lwork = (int) small_work_doublecomplex[0].real;
    liwork = small_work_int[0];
    lrwork = (int) small_work_double[0];
    doublecomplex* work = alloc_memory(sizeof(doublecomplex) * lwork);
    double* rwork = alloc_memory(sizeof(double) * lrwork);
    int* iwork = alloc_memory(sizeof(int) * liwork);
    zheevr_(&jobz, &range, &uplo, &n, a, &lda, &vl, &vu, &il, &iu, &abstol, &m, w, z, &ldz, isuppz, work, &lwork, rwork, &lrwork, iwork, &liwork, info);
    free(iwork);
    free(rwork);
    free(work);
    free(isuppz);
}

此函数在我的应用程序中被调用了数十万次,以对相同矩阵大小的复矩阵“a”(参数名称遵循该函数的 Fortran 约定)进行对角化。我认为工作数组的大小在大多数情况下都是相同的,因为对角化矩阵将具有相同的结构。我的问题是:

  1. 重复的 alloc/free(“alloc_memory”是 glibc 的 malloc 的简单包装器)调用会影响性能吗?会影响到什么程度?
  2. 免费的顺序重要吗?我应该先释放最后分配的数组,还是最后释放?

【问题讨论】:

  • 只是评论,因为我无法为您提供直接的答案。在问题1中,您可以通过使用valgrind工具“callgrind”运行您的应用程序来获得答案。
  • 出于某种原因(可能是因为我将 C 代码链接到 Fortran 代码),“callgrind”死机并显示消息“vex amd64->IR:未处理的指令字节:0xF2 0xF 0x12 0x58”跨度>

标签: c performance memory-management glibc


【解决方案1】:
  • 可以使用 C99 吗? (答案:是的,您已经在使用 C99 符号 - 在需要时声明变量。)
  • 数组的大小是否合理(不是太大)?

如果两个答案都是“是”,请考虑使用 VLA - 可变长度数组:

void zheevr(char jobz, char range, char uplo, int n, doublecomplex* a, int lda, double vl, double vu, int il, int iu, double abstol, double* w, doublecomplex* z, int ldz, int* info)
{
    int m;
    int lwork = -1;
    int liwork = -1;
    int lrwork = -1;
    int isuppz[2*n];
    zheevr_(&jobz, &range, &uplo, &n, a, &lda, &vl, &vu, &il, &iu, &abstol, &m, w, z, &ldz, isuppz, small_work_doublecomplex, &lwork, small_work_double, &lrwork, small_work_int, &liwork, &info);
    lwork = (int) small_work_doublecomplex[0].real;
    liwork = small_work_int[0];
    lrwork = (int) small_work_double[0];
    doublecomplex work[lwork];
    double rwork[lrwork];
    int iwork[liwork];
    zheevr_(&jobz, &range, &uplo, &n, a, &lda, &vl, &vu, &il, &iu, &abstol, &m, w, z, &ldz, isuppz, work, &lwork, rwork, &lrwork, iwork, &liwork, info);
}

使用 VLA 的一个好处是您无需做任何事情。

(未经测试的代码!)

【讨论】:

  • 谢谢!我不知道我可以在 C99 中做这样的事情。我可以安全地将这样的数组传递给 Fortran 代码吗?
  • 也许......也许不是......堆栈只是另一个内存区域,但可能存在问题。你必须测试它。有一件事是肯定的,如果它与 Stack 内存配合得很好,那么您将无法获得比这更快的速度。
  • 我已经测试过了,它在 Fortran 上运行良好,当然我不能将它用于非常大的矩阵。
  • 在 Windows 上,我知道您可以在编译时设置堆栈大小(我从来不需要这种想法),您可能想检查一下。
【解决方案2】:

1) 是的,他们可以。

2) 任何理智的 libc 都不应该担心 free() 的顺序。性能方面也应该无关紧要。

我建议从这个函数中删除内存管理——所以调用者将提供矩阵大小并分配临时缓冲区。如果从相同大小的矩阵的相同位置调用此函数,将显着减少 malloc 的数量。

【讨论】:

  • 感谢 2),我对此表示怀疑。将内存管理移到别处的问题是代码开始看起来非常难看。
  • 可以编写一个理智的内存分配器,在 free() 处进行快速块合并测试,查看“下一个”块是否已经空闲,如果是则合并。释放的顺序会对碎片产生轻微的影响,因为如果你以相反的顺序释放,块将立即合并。是否有人曾经在 libc 实现中这样做是另一个问题,但这并不是 IMO 的疯狂。
  • free 的任何合理实现总是合并下一个和上一个空闲块,因此是O(1)
【解决方案3】:

它肯定会影响性能-youb只能通过时间确定确定。要创建避免大多数分配的版本,请分配给静态指针并记住另一个静态整数的大小。如果下一次调用使用相同的大小,只需重用上次分配的内容。仅当您需要创建新矩阵时才释放任何内容,因为大小已更改。

请注意,此解决方案仅适用于单线程代码。

【讨论】:

  • 我已经在另一个 LAPACK 函数的包装器中实现了这个解决方案,该函数需要更少的工作数组(每次调用只需 2 个分配),ZHEEV。令人惊讶的是,“幼稚”分配和静态数组之间没有性能差异。
  • 根据平台的不同,C malloc 库可能已经做了类似的事情。 Google for 'Doug Lea Allocator'。
【解决方案4】:

好的。您很快就会得到分析器的答案。如果你有 AMD 机器,我强烈推荐免费的 AMD 的 CodeAnalyst。

至于您的内存问题,我认为在这种情况下您可以使用本地内存管理。只需确定您可以为此功能分配的最大内存数量即可。 接下来,您声明一个静态缓冲区,并且您使用它有点像编译器如何处理堆栈。我曾经在 VirtualAlloc 上做了一个这样的包装器,它非常快。

【讨论】:

    【解决方案5】:

    如果您分配相同大小的项目数十万次,那么为什么不只维护一个对象堆(因为这些似乎相对简单,即不包含指向其他已分配内存的指针)并释放到您自己的堆(或实际上是堆栈)?

    堆可以使用 glib malloc 延迟分配新对象,但释放时只需将项目推送到堆上。当您需要分配时,如果有可用的已释放对象,则只需分配该对象即可。

    这也将节省您对分配的多次调用(因为您不需要进行任何分配,而且看起来您的例程多次调用 malloc)并且至少在重新- 使用过的内存。当然,初始分配(以及程序运行时需要扩展此内存时的其他分配)可能会导致碎片,但如果您真的担心这一点,您可以运行一些统计数据并找到您的平均/最大/典型大小在运行期间堆并在程序启动时立即预分配,避免碎片。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-10-08
      • 2011-06-23
      • 2015-11-17
      • 1970-01-01
      • 2013-04-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多