boost::mpi 作为模板类的静态成员

Posted

技术标签:

【中文标题】boost::mpi 作为模板类的静态成员【英文标题】:boost::mpi as static member of templated class 【发布时间】:2020-08-22 11:33:28 【问题描述】:

我想在我的班级中使用boost::mpi 通信器,因为我希望我的班级负责所有 MPI 调用。我使用这种样式使它们成为我班级的静态成员。

// works.cpp
// mpic++ -o works works.cpp -lboost_mpi
#include <boost/mpi.hpp>
#include <iostream>

class Example 
  static boost::mpi::environment env;
  static boost::mpi::communicator world; 
 public:
  Example() 
    std::cout << world.rank() << std::endl;
  
;

boost::mpi::environment Example::env;
boost::mpi::communicator Example::world;

int main() 
  auto e = Example();

这很好用,例如,mpirun -n 4 ./works 按预期打印数字 0 到 3。后来我想为我的课程模板。一开始我还担心如何初始化我的静态成员变量,但看了这个answer,这表明一切都很好

// fails.cpp
// mpic++ -o fails fails.cpp -lboost_mpi
#include <boost/mpi.hpp>
#include <iostream>

template<typename T>
class Example 
  static boost::mpi::environment env;
  static boost::mpi::communicator world; 
 public:
  Example() 
    std::cout << world.rank() << std::endl;
  
;

template <typename T>
boost::mpi::environment Example<T>::env;

template <typename T>
boost::mpi::communicator Example<T>::world;

int main() 
  auto e = Example<double>();

然而,这实际上给了我

$ mpirun -n 4 ./fails
*** The MPI_Comm_rank() function was called before MPI_INIT was invoked.
*** This is disallowed by the MPI standard.
*** Your MPI job will now abort.
...

这里出了点问题,但是什么?为什么?我想我在这里误解了一些事情。

【问题讨论】:

【参考方案1】:

您的课程中需要boost::mpi::environment 实例有什么特别的原因吗?它只是围绕 MPI 当前单例性质的一个可怕的 OO 包装器 - 您可以通过调用 MPI_Init() 仅初始化 MPI 一次,然后通过调用 MPI_Finalize() 仅完成一次它(至少,直到 MPI 会话进入未来标准的版本)。如果您尝试在MPI_Init() 之前执行与 MPI 相关的任何操作,则会收到错误消息(少数有效的信息查询调用除外)。如果您在MPI_Finalize() 之后尝试执行与 MPI 相关的任何操作,则会收到错误消息。如果在调用 MPI_Init() 之后没有调用 MPI_Finalize() 就退出了单个 rank,则整个 MPI 作业通常会崩溃,因为启动器认为发生了错误并杀死了其余的 rank。

boost::mpi::environment 实例的唯一目的是确保 MPI 已初始化并且不会未完成,并且所有这些都由对象的生命周期控制。因此,您必须将它的一个实例放置在该实例将比您的程序中的任何 MPI 活动更早且寿命更长的位置,而最好的位置是 main() 函数。此外,虽然 MPI-2 实现现在很流行,并且您可以安全地使用 environment 构造函数的无参数变体,但为了兼容性起见,最好使用从 main() 获取 argcargv 的那个。当然,除非您正在编写库。

请注意,environment 对象会检查 MPI 在创建时是否已经初始化,如果是,则在销毁时它们不会完成它,但如果它们要初始化它,它们将完成它。因此,您不应该有两个生命周期不重叠的实例。因此,在处理类的全局实例时,必须确保正确的构造函数和析构函数调用顺序。所以,保持简单,只需在 main() 函数中创建一个实例。

boost::mpi::communicator 不是这种情况。它的默认构造函数只是包装了MPI_COMM_WORLD 通信器句柄,这是一个运行时常量,指的是单例 MPI 世界通信器。由于默认构造函数的包装模式是comm_attach,因此实例在销毁时不会尝试释放MPI_COMM_WORLD,因此您可以在代码的任何部分拥有任意数量的这些。请记住,大多数实例方法仅在 MPI 环境初始化之后和最终确定之前起作用,这定义了实际世界通信器对象的生命周期(MPI 实现中的那个,而不是boost::mpi::communicator 的实例) .

您甚至不需要communicator 的静态实例,因为您只需要它来访问世界通信器。您只保存了一个到 new 和一个到 delete 的呼叫。

我会这样写你的代码:

#include <boost/mpi.hpp>
#include <iostream>

template<typename T>
class Example 
  boost::mpi::communicator world; 
public:
  Example() 
    std::cout << world.rank() << std::endl;
  
;

int main(int argc, char **argv) 
  boost::mpi::environment(argc, argv);
  auto e = Example<double>();

它按预期工作:

$ mpic++ -o works works.cc -lboost_mpi
$ mpiexec -n 4 ./works
2
0
3
1

【讨论】:

我同意。我只是有问题的环境吗?在其他地方构建通信器可以吗? @innisfree 有一个单例世界通信器MPI_COMM_WORLD,但它不是由communicator 类构造的——它只是在调用默认构造函数时被包装。这意味着您不能在 MPI 初始化之前构造此类对象,因为那是 MPI_COMM_WORLD 获取其实际值的时候(至少对于 Open MPI;它在基于 MPICH 的实现中具有特殊的常量值)。 @innisfree 我对 Open MPI 进行了更正 - MPI_COMM_WORLD 是静态结构的地址,因此它的值在初始化后不会改变。事实上,标准保证句柄是常量。【参考方案2】:

您的程序的两个版本都会导致未定义的行为,如 boost::mpi documentation 中所述。第一个工作的事实显然是一个意外。怎么了?正是这样:

在全局范围内声明 mpi::environment 是未定义的行为。

所以简而言之,mpi::environment 应该在 main 的开头创建。

【讨论】:

啊,所以我应该将全局范围解释为包含我的私有静态成员变量? 它在技术上不是全局范围,但由于static 成员和全局变量之间的实际差异几乎没有,我会这样解释它。特别是如果被告知environment 使用argcargv 是的,有道理,谢谢。对我刚刚写给自己的(部分)答案的任何想法将不胜感激。例如,该代码的行为是否仍未定义? @bartop,文档具有误导性。它不使用argcargv,除非调用它们的重载构造函数。这与不允许将 NULL 传递给 MPI_Init() 的 MPI-2 之前的实现有关。【参考方案3】:

好吧,在这个answer 之后,可以通过在main 中创建worldenv 的单独实例,但是否可取,我不知道。例如,这似乎按预期工作:

#include <boost/mpi.hpp>
#include <iostream>

template<typename T>
class Example 
  static boost::mpi::environment env;
  static boost::mpi::communicator world; 
 public:
  Example() 
    std::cout << world.rank() << std::endl;
  
;

template <typename T>
boost::mpi::environment Example<T>::env;

template <typename T>
boost::mpi::communicator Example<T>::world;

int main() 
  boost::mpi::environment env_in_main;
  boost::mpi::communicator world_in_main; 
  auto e = Example<double>();

不过,实际上,大多数功能都可以使用通信器world 来实现。几乎不需要将 env 设为类成员,因此最好的解决方案可能是将 world 保留为类成员,而不是 env。

【讨论】:

我会说它仍然是 UB 或至少使用未记录的功能,总而言之很容易导致 UB。我会坚持记录在案的内容

以上是关于boost::mpi 作为模板类的静态成员的主要内容,如果未能解决你的问题,请参考以下文章

SFINAE模板类复杂类型的静态成员定义

内部类之静态内部类

练习7.567.58

类的非静态成员函数作为线程函数的注意事项

C++类的大小计算汇总

java里的静态成员变量是放在了堆内存还是栈