【问题标题】:C++ std::vector for HPC?用于 HPC 的 C++ std::vector?
【发布时间】:2017-01-25 14:11:05
【问题描述】:

我正在将一个执行数字模拟的程序从 FORTRAN 翻译成 C++。

我必须处理 800MB 大小的双倍大矩阵。 这个

double M[100][100][100][100];

给出一个分段错误,因为堆栈不是那么大。 使用 new,delete 很尴尬,因为我需要四个 for 循环来分配我的数组,甚至释放它。

std::array 在堆栈中,所以不好。 std::vector 将是一个不错的选择,所以

第一个问题 std::vector 适合快速模拟还是

vector<vector<vector<vector<int,100>,100>,100>,100> 

会携带大量无用的繁重数据吗?

第二个问题 您知道我可以使用的任何其他数据结构吗?也许有来自 boost 的东西。

目前我只是在使用这个解决方案:

double * M = new double [100000000];

我正在手动访问我需要的条目。 如果我没有找到任何其他高性能解决方案,我将编写一个自动管理最后一个方法的类。

第三个问题你认为这会显着降低性能吗?

【问题讨论】:

  • 单个分配的块是矩阵的不错选择。
  • std::vector&lt;double&gt; M(100000000); 可能是最好的。
  • 你应该看看 Boost 多维数组库:boost.org/doc/libs/1_61_0/libs/multi_array/doc/user.html
  • 重要的是要知道 STL 容器有内存开销(它们的成本不仅仅是存储的元素),因此嵌套它们(vector>)通常不是一个好主意。我建议您完全按照您的做法(单块内存)。为了提高代码的易读性,您可以将其打包在一个类中并重载 operator(int,int,int,int) 以检索元素。

标签: c++ vector hpc numerical-computing


【解决方案1】:

在某些情况下,将 1-D 指针转换为 4-D 矩形数组以启用笛卡尔索引(而不是线性索引)可能很有用:

#include <cstdio>

#define For( i, n ) for( int i = 0; i < n; i++ )

double getsum( double *A, int *n, int loop )
{
    // Cast to 4-D array.
    typedef double (* A4d_t)[ n[2] ][ n[1] ][ n[0] ];
    A4d_t A4d = (A4d_t) A;

    // Fill the array with linear indexing.
    int ntot = n[0] * n[1] * n[2] * n[3];
    For( k, ntot ) A[ k ] = 1.0 / (loop + k + 2);

    // Calc weighted sum with Cartesian indexing.
    double s = 0.0;
    For( i3, n[3] )
    For( i2, n[2] )
    For( i1, n[1] )
    For( i0, n[0] )
        s += A4d[ i3 ][ i2 ][ i1 ][ i0 ] * (i0 + i1 + i2 + i3 + 4);

    return s;
}

int main()
{
    int n[ 4 ] = { 100, 100, 100, 100 };

    double *A = new double [ n[0] * n[1] * n[2] * n[3] ];

    double ans = 0.0;
    For( loop, 10 )
    {
        printf( "loop = %d\n", loop );
        ans += getsum( A, n, loop );
    }
    printf( "ans = %30.20f\n", ans );
    return 0;
}

在 Mac OSX 10.9 上使用 g++-6.0 -O3 需要 5.7 秒。将性能与基于vector&lt;vector&lt;...&gt;&gt; 或自定义数组视图类的性能进行比较可能会很有趣。 (我之前尝试过后者,当时上面的数组转换比我的(天真的)数组类要快一些。)

【讨论】:

    【解决方案2】:

    您可能需要考虑std::valarray,它旨在与 FORTRAN 竞争。它将元素存储为平面数组并支持数学运算,以及用于切片和间接访问的运算。

    无论如何,这听起来像是您的计划。尽管即使是 manpage 也暗示可能有更灵活的替代方案。

    【讨论】:

      【解决方案3】:

      程序员倾向于通过首先编写更多代码来解决每个问题。然后必须对其进行维护。每个问题不是钉子...

      更多代码并不是这里最简单、最有效的解决方案。更多代码也可能会生成一个更慢的可执行文件。

      堆栈内存就是内存——它与堆内存没有什么不同。它只是由进程以不同的方式管理,并且受到不同的资源限制。一个进程在其堆栈上使用 1 GB 内存,还是从其堆中使用 1 GB 内存,对于操作系统来说并没有真正的区别。

      在这种情况下,堆栈大小限制可能是人为的配置设置。在 Linux 系统上,可以为 shell 及其子进程重置堆栈大小限制:

      bash-4.1$ ulimit -s unlimited
      bash-4.1$ ulimit -s
      unlimited
      bash-4.1$
      

      更多详情请见this question and its answers

      所有符合 POSIX 的系统都应该具有类似的功能,因为堆栈大小限制为 POSIX-standard resource limit

      此外,您可以很容易地运行具有任意大堆栈的线程:

      #include <pthread.h>
      #include <stdlib.h>
      #include <string.h>
      #include <sys/mman.h>
      #include <stdio.h>
      
      void *threadFunc( void *arg )
      {
          double array[1024][1024][64];
          memset( array, 0, sizeof( array ) );
          return( NULL );
      }
      int main( int argc, char **argv )
      {
          // create and memset the stack lest the child thread start thrashing
          // the machine with "try to run/page fault/map page" cycles
          // the memset will create the physical page mappings
          size_t stackSize = strtoul( argv[ 1 ] ? argv[ 1 ] : "1073741824",
              NULL, 0 );    
          void *stack = mmap( 0, stackSize, PROT_READ | PROT_WRITE,
              MAP_PRIVATE | MAP_ANON, -1, 0 );
          memset( stack, 0, stackSize );
      
          // create a large stack for the child thread
          pthread_attr_t attr;    
          pthread_attr_init( &attr );
          pthread_attr_setstacksize( &attr, stackSize );
          pthread_attr_setstackaddr( &attr, stack );
      
          pthread_t tid;
          pthread_create( &tid, &attr, threadFunc, NULL );
          void *result;
          pthread_join( tid, &result );
          return( 0 );
      }
      

      已省略错误检查。

      如果您在运行已编译的程序之前运行ulimit -s unlimited(当然,如果机器有足够的虚拟内存......),这也有效:

      #include <string.h>
      
      int main( int argc, char **argv )
      {
          double array[1024][1024][64];
          memset( array, 0, sizeof( array ) );
      
          return( 0 );
      }
      

      【讨论】:

      • “栈内存就是内存——它和堆内存没有区别。”这绝对不是真的。编译器(以及编译后的程序)如何处理这两种类型的内存是非常不同的。堆需要系统调用、一些检查,并且更容易受到缓存未命中的影响。简单说几句
      • 在效率上和new和delete的内存管理一样吗?
      • @BiagioFesta - 一旦进程获得内存,它就是内存。 进程如何从操作系统获取内存不太可能对长时间运行和/或计算密集型进程的性能产生太大影响。然而,进程内部管理内存的方式会极大地影响性能。
      • @LucaMarconato 就效率而言,它与使用new和delete的内存管理相同吗?堆栈内存可以new快得多 /delete - 由于堆栈与在其上运行的线程绑定,因此访问完全是无争用和无锁的。一个限制是,一旦在返回中声明它们的函数,堆栈上的局部变量就不再存在(这是“简单”的解释......)。多线程进程的另一个限制是每个线程都需要自己的专用堆栈,因此堆栈内存使用的空间效率可能低于使用单个进程范围的堆。
      【解决方案4】:

      使用堆栈上的东西肯定会更有效。进程内存受操作系统(堆栈+堆)限制,您的问题是在大多数情况下您可能会超出分配给进程的内存。

      要解决内存限制,我建议您查看stxxl。它是一个库,它实现了大多数 STL 容器和算法,但在需要时使用外部存储器。当然这会影响性能...

      【讨论】:

      • “在大多数情况下”超过 64 位地址空间并非易事。
      【解决方案5】:

      我认为这个问题的答案是基于意见的。 “good”的概念严格依赖于数据结构的使用。

      无论如何,如果元素的数量在执行期间没有改变,而您的问题实际上是内存访问,那么在我看来,最好的解决方案是一个连续的块数组。

      通常在这些情况下,我的选择是将简单的T* data = new T[SIZE]; 封装到一个可以正确处理访问的类中。

      指针的使用让我对内存控制感觉更舒服一点,但实际上std::vector&lt;T&gt;实际上是一回事。


      根据您在问题中提供的知识,我只能说这些。无论如何,我还可以建议您注意数据的使用。

      例如:为了最大化您的应用程序的性能,请尝试利用缓存并避免错过。因此,您可以尝试了解您的数据是否存在一些“访问模式”,或者,甚至,您是否可以考虑多线程上下文来扩展您的问题。


      总而言之,在我看来,double 的连续向量通常是最佳选择。这回答了你的问题。但是如果你关心性能,你应该考虑如何尽可能地利用缓存处理器机制(比如多线程)。

      【讨论】:

      • 所以你有在 Fortran 中工作的代码,你想用 C 重写它,但它不会运行......但目的是什么?您可以将工作中的 Fortran 与 C 链接...这会给您带来任何好处吗?
      猜你喜欢
      • 1970-01-01
      • 2011-01-21
      • 1970-01-01
      • 2015-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-01
      • 2022-09-30
      相关资源
      最近更新 更多