Boost C++ 库

...世界上最受推崇和专业设计的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

PrevUpHomeNext

教程

点对点通信
集体操作
用户定义的数据类型
通信器
线程
将结构与内容分离
性能优化

一个 Boost.MPI 程序由许多协同工作的进程组成(可能在不同的计算机上运行),这些进程通过传递消息在彼此之间进行通信。Boost.MPI 是一个库(与较低级别的 MPI 一样),而不是一种语言,因此 Boost.MPI 的第一步是创建一个 mpi::environment 对象,该对象初始化 MPI 环境并启用进程之间的通信。mpi::environment 对象使用程序参数(它可能会修改)在您的主程序中进行初始化。此对象的创建初始化 MPI,其销毁将最终确定 MPI。在绝大多数 Boost.MPI 程序中,mpi::environment 的实例将在程序开始时在 main 中声明。

[Warning] 警告

在全球范围内声明 mpi::environment 是未定义的行为。 [10]

与 MPI 的通信始终通过通信器进行,可以通过简单地默认构造 mpi::communicator 类型的对象来创建通信器。然后可以查询此通信器以确定正在运行的进程数(通信器的“大小”)并为每个进程提供一个唯一的编号,从零到通信器的大小(即进程的“等级”)

#include <boost/mpi/environment.hpp>
#include <boost/mpi/communicator.hpp>
#include <iostream>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;
  std::cout << "I am process " << world.rank() << " of " << world.size()
            << "." << std::endl;
  return 0;
}

例如,如果您使用 7 个进程运行此程序,您将收到如下输出

I am process 5 of 7.
I am process 0 of 7.
I am process 1 of 7.
I am process 6 of 7.
I am process 2 of 7.
I am process 4 of 7.
I am process 3 of 7.

当然,进程每次可以以不同的顺序执行,因此等级可能不是严格递增的。更有趣的是,文本可能会完全乱码,因为一个进程可能在另一个进程完成写入“of 7.”之前开始写入“I am a process”。

如果您仍然有一个仅支持 MPI 1.1 的 MPI 库,您将需要将命令行参数传递给环境构造函数,如此示例所示

#include <boost/mpi/environment.hpp>
#include <boost/mpi/communicator.hpp>
#include <iostream>
namespace mpi = boost::mpi;

int main(int argc, char* argv[])
{
  mpi::environment env(argc, argv);
  mpi::communicator world;
  std::cout << "I am process " << world.rank() << " of " << world.size()
            << "." << std::endl;
  return 0;
}

作为消息传递库,MPI 的主要目的是将消息从一个进程路由到另一个进程,即点对点。MPI 包含可以发送消息、接收消息和查询消息是否可用的例程。每个消息都有一个源进程、一个目标进程、一个标签和一个包含任意数据的有效负载。源进程和目标进程分别是消息的发送者和接收者的等级。标签是整数,允许接收者区分来自同一发送者的不同消息。

以下程序使用两个 MPI 进程将“Hello, world!”写入屏幕 (hello_world.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <string>
#include <boost/serialization/string.hpp>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;

  if (world.rank() == 0) {
    world.send(1, 0, std::string("Hello"));
    std::string msg;
    world.recv(1, 1, msg);
    std::cout << msg << "!" << std::endl;
  } else {
    std::string msg;
    world.recv(0, 0, msg);
    std::cout << msg << ", ";
    std::cout.flush();
    world.send(0, 1, std::string("world"));
  }

  return 0;
}

第一个处理器(等级 0)使用标签 0 将消息“Hello”传递给第二个处理器(等级 1)。第二个处理器打印它接收到的字符串,以及一个逗号,然后使用不同的标签将消息“world”传递回处理器 0。然后,第一个处理器写入此消息以及“!”并退出。所有发送都使用 communicator::send 方法完成,所有接收都使用相应的 communicator::recv 调用。

默认的 MPI 通信操作——sendrecv——可能必须等到整个传输完成才能返回。有时,这种阻塞行为会对性能产生负面影响,因为发送者在等待传输发生时可能会执行有用的计算。然而,更重要的是必须同时发生多个通信操作的情况,例如,一个进程将同时发送和接收。

让我们回顾一下前一个部分中的“Hello, world!”程序。此程序的核心传输两条消息

if (world.rank() == 0) {
  world.send(1, 0, std::string("Hello"));
  std::string msg;
  world.recv(1, 1, msg);
  std::cout << msg << "!" << std::endl;
} else {
  std::string msg;
  world.recv(0, 0, msg);
  std::cout << msg << ", ";
  std::cout.flush();
  world.send(0, 1, std::string("world"));
}

第一个进程将消息传递给第二个进程,然后准备接收消息。第二个进程以相反的顺序执行发送和接收。然而,此事件序列只是一个序列——这意味着基本上没有并行性。我们可以使用非阻塞式通信来确保两条消息同时传输 (hello_world_nonblocking.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <string>
#include <boost/serialization/string.hpp>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;

  if (world.rank() == 0) {
    mpi::request reqs[2];
    std::string msg, out_msg = "Hello";
    reqs[0] = world.isend(1, 0, out_msg);
    reqs[1] = world.irecv(1, 1, msg);
    mpi::wait_all(reqs, reqs + 2);
    std::cout << msg << "!" << std::endl;
  } else {
    mpi::request reqs[2];
    std::string msg, out_msg = "world";
    reqs[0] = world.isend(0, 1, out_msg);
    reqs[1] = world.irecv(0, 0, msg);
    mpi::wait_all(reqs, reqs + 2);
    std::cout << msg << ", ";
  }

  return 0;
}

我们已将对 communicator::sendcommunicator::recv 成员的调用替换为对其非阻塞对应项 communicator::isendcommunicator::irecv 的类似调用。前缀i表示操作立即返回一个 mpi::request 对象,该对象允许查询通信请求的状态(请参阅 test 方法)或等待直到其完成(请参阅 wait 方法)。可以使用 wait_all 操作同时完成多个请求。

[Important] 重要提示

关于通信完成/进度:MPI 标准要求用户保留非阻塞通信的请求句柄,并调用“wait”操作(或成功测试完成情况)以完成发送或接收。与大多数 C MPI 实现不同,后者允许用户丢弃非阻塞发送的请求,Boost.MPI 要求用户调用“wait”或“test”,因为请求对象可能包含必须保留到发送完成的临时缓冲区。此外,MPI 标准不保证接收在调用“wait”或“test”之前取得任何进展,尽管大多数 C MPI 实现确实允许接收在调用“wait”或“test”之前取得进展。另一方面,Boost.MPI 通常需要“test”或“wait”调用才能取得进展。更具体地说,Boost.MPI 保证多次调用“test”最终将完成通信(这是因为串行化通信可能是多步操作)。

如果您多次运行此程序,您可能会看到一些奇怪的结果:即,某些运行会产生

Hello, world!

而其他运行会产生

world!
Hello,

甚至一些“Hello”和“world”字母的乱码版本。这表明程序中存在一些并行性,因为在两条消息(同时)传输之后,两个进程将并发执行其打印语句。对于性能和正确性,非阻塞式通信操作对于许多使用 MPI 的并行应用程序至关重要。

点对点操作是 Boost.MPI 中的核心消息传递原语。然而,许多消息传递应用程序还需要更高级别的通信算法,这些算法组合或汇总存储在许多不同进程上的数据。这些算法支持许多常见任务,例如“将此值广播到所有进程”、“计算所有处理器上的值的总和”或“查找全局最小值”。

broadcast 算法是迄今为止最简单的集体操作。它将单个进程中的值广播到 communicator 中的所有其他进程。例如,以下程序将“Hello, World!”从进程 0 广播到每个其他进程。 (hello_world_broadcast.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <string>
#include <boost/serialization/string.hpp>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;

  std::string value;
  if (world.rank() == 0) {
    value = "Hello, World!";
  }

  broadcast(world, value, 0);

  std::cout << "Process #" << world.rank() << " says " << value
            << std::endl;
  return 0;
}

使用七个进程运行此程序将产生如下结果

Process #0 says Hello, World!
Process #2 says Hello, World!
Process #1 says Hello, World!
Process #4 says Hello, World!
Process #3 says Hello, World!
Process #5 says Hello, World!
Process #6 says Hello, World!

gather 集体操作将通信器中每个进程生成的值收集到“根”进程(由 gather 的参数指定)上的值向量中。向量中的第 /i/ 个元素将对应于从第 /i/ 个进程收集的值。例如,在以下程序中,每个进程计算其自己的随机数。所有这些随机数都在进程 0(在本例中为“根”)处收集,该进程打印出与每个处理器对应的值。 (random_gather.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;

  std::srand(time(0) + world.rank());
  int my_number = std::rand();
  if (world.rank() == 0) {
    std::vector<int> all_numbers;
    gather(world, my_number, all_numbers, 0);
    for (int proc = 0; proc < world.size(); ++proc)
      std::cout << "Process #" << proc << " thought of "
                << all_numbers[proc] << std::endl;
  } else {
    gather(world, my_number, 0);
  }

  return 0;
}

使用七个进程执行此程序将产生如下输出。虽然随机值会因一次运行而异,但输出中进程的顺序将保持不变,因为只有进程 0 写入 std::cout

Process #0 thought of 332199874
Process #1 thought of 20145617
Process #2 thought of 1862420122
Process #3 thought of 480422940
Process #4 thought of 1253380219
Process #5 thought of 949458815
Process #6 thought of 650073868

gather 操作将每个进程的值收集到一个进程的向量中。相反,如果需要将每个进程的值收集到每个进程上的相同向量中,请使用 all_gather 算法,该算法在语义上等同于调用 gather,后跟 broadcast 结果向量。

scatter 集体操作将通信器中“根”进程中向量的值散播到通信器中所有进程的值中。向量中的第 /i/ 个元素将对应于第 /i/ 个进程接收的值。例如,在以下程序中,根进程生成一个随机数向量,并将一个值发送给每个将打印它的进程。 (random_scatter.cpp)

#include <boost/mpi.hpp>
#include <boost/mpi/collectives.hpp>
#include <iostream>
#include <cstdlib>
#include <vector>

namespace mpi = boost::mpi;

int main(int argc, char* argv[])
{
  mpi::environment env(argc, argv);
  mpi::communicator world;

  std::srand(time(0) + world.rank());
  std::vector<int> all;
  int mine = -1;
  if (world.rank() == 0) {
    all.resize(world.size());
    std::generate(all.begin(), all.end(), std::rand);
  }
  mpi::scatter(world, all, mine, 0);
  for (int r = 0; r < world.size(); ++r) {
    world.barrier();
    if (r == world.rank()) {
      std::cout << "Rank " << r << " got " << mine << '\n';
    }
  }
  return 0;
}

使用七个进程执行此程序将产生如下输出。虽然随机值会因一次运行而异,但输出中进程的顺序将保持不变,因为存在屏障。

Rank 0 got 1409381269
Rank 1 got 17045268
Rank 2 got 440120016
Rank 3 got 936998224
Rank 4 got 1827129182
Rank 5 got 1951746047
Rank 6 got 2117359639

reduce 集体操作将每个进程的值汇总到用户指定的“根”进程中的单个值中。Boost.MPI reduce 操作在精神上类似于 STL accumulate 操作,因为它接受一系列值(每个进程一个)并通过函数对象将它们组合起来。例如,我们可以在每个进程中随机生成值,并通过调用 reduce 计算所有进程的最小值 (random_min.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <cstdlib>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;

  std::srand(time(0) + world.rank());
  int my_number = std::rand();

  if (world.rank() == 0) {
    int minimum;
    reduce(world, my_number, minimum, mpi::minimum<int>(), 0);
    std::cout << "The minimum value is " << minimum << std::endl;
  } else {
    reduce(world, my_number, mpi::minimum<int>(), 0);
  }

  return 0;
}

mpi::minimum<int> 的使用表明应计算最小值。mpi::minimum<int> 是一个二元函数对象,它通过 < 比较其两个参数并返回较小的值。任何关联的二元函数或函数对象都将起作用,前提是它是无状态的。例如,要使用 reduce 连接字符串,可以使用函数对象 std::plus<std::string> (string_cat.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <string>
#include <functional>
#include <boost/serialization/string.hpp>
namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;

  std::string names[10] = { "zero ", "one ", "two ", "three ",
                            "four ", "five ", "six ", "seven ",
                            "eight ", "nine " };

  std::string result;
  reduce(world,
         world.rank() < 10? names[world.rank()]
                          : std::string("many "),
         result, std::plus<std::string>(), 0);

  if (world.rank() == 0)
    std::cout << "The result is " << result << std::endl;

  return 0;
}

在此示例中,我们为每个进程计算一个字符串,然后执行归约,将所有字符串连接成一个长字符串。使用七个处理器执行此程序会产生以下输出

The result is zero one two three four five six
用于归约的二元运算

任何类型的二元函数对象都可以与 reduce 一起使用。例如,C++ 标准 <functional> 标头和 Boost.MPI 标头 <boost/mpi/operations.hpp> 中有很多这样的函数对象。或者,您可以创建自己的函数对象。与 reduce 一起使用的函数对象必须是关联的,即 f(x, f(y, z)) 必须等效于 f(f(x, y), z)。如果它们也是可交换的(即,f(x, y) == f(y, x)),则 Boost.MPI 可以使用更有效的 reduce 实现。要声明函数对象是可交换的,您需要专门化类 is_commutative。例如,我们可以通过告诉 Boost.MPI 字符串连接是可交换的来修改前面的示例

namespace boost { namespace mpi {

  template<>
  struct is_commutative<std::plus<std::string>, std::string>
    : mpl::true_ { };

} } // end namespace boost::mpi

通过在 main() 之前添加此代码,Boost.MPI 将假定字符串连接是可交换的,并为 reduce 操作采用不同的并行算法。使用此算法,程序在使用七个进程运行时输出以下内容

The result is zero one four five six two three

请注意,结果字符串中的数字顺序不同:这是 Boost.MPI 重新排序操作的直接结果。在这种情况下,结果与不可交换结果不同,因为字符串连接不是可交换的:f("x", "y")f("y", "x") 不同,因为参数顺序很重要。对于真正可交换的操作(例如,整数加法),更高效的可交换算法将产生与不可交换算法相同的结果。Boost.MPI 还执行从 <functional> 中的函数对象到 MPI 预定义的 MPI_Op 值(例如,MPI_SUMMPI_MAX)的直接映射;如果您有自己的函数对象可以利用此映射,请参阅类模板 is_mpi_op

[Warning] 警告

由于底层的 MPI 限制,重要的是要注意操作必须是无状态的。

所有进程变体

gather 一样,reduce 有一个名为 all_reduce 的“all”变体,它执行归约操作并将结果广播到所有进程。例如,此变体在建立全局最小值或最大值时很有用。

以下代码 (global_min.cpp) 显示了 random_min.cpp 示例的广播版本

#include <boost/mpi.hpp>
#include <iostream>
#include <cstdlib>
namespace mpi = boost::mpi;

int main(int argc, char* argv[])
{
  mpi::environment env(argc, argv);
  mpi::communicator world;

  std::srand(world.rank());
  int my_number = std::rand();
  int minimum;

  mpi::all_reduce(world, my_number, minimum, mpi::minimum<int>());

  if (world.rank() == 0) {
      std::cout << "The minimum value is " << minimum << std::endl;
  }

  return 0;
}

在该示例中,我们同时提供输入和输出值,需要两倍的空间,这可能会成为问题,具体取决于传输数据的大小。如果不需要保留输入值,则可以省略输出值。在这种情况下,输入值将被输出值覆盖,并且 Boost.MPI 可以在某些情况下使用更节省空间的解决方案(使用 MPI C 映射的 MPI_IN_PLACE 标志)来实现该操作,如下例所示 (in_place_global_min.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <cstdlib>
namespace mpi = boost::mpi;

int main(int argc, char* argv[])
{
  mpi::environment env(argc, argv);
  mpi::communicator world;

  std::srand(world.rank());
  int my_number = std::rand();

  mpi::all_reduce(world, my_number, mpi::minimum<int>());

  if (world.rank() == 0) {
      std::cout << "The minimum value is " << my_number << std::endl;
  }

  return 0;
}

在前面的示例中包含 boost/serialization/string.hpp 非常重要:它使 std::string 类型的值可序列化,以便可以使用 Boost.MPI 传输它们。通常,内置 C++ 类型(intfloat、字符等)可以直接通过 MPI 传输,而用户定义和库定义的类型需要先序列化(打包)为适合传输的格式。Boost.MPI 依赖于 Boost.Serialization 库来序列化和反序列化数据类型。

对于标准库定义的类型(例如 std::stringstd::vector)和 Boost 中的某些类型(例如 boost::variant),Boost.Serialization 库已包含所有必需的序列化代码。在这些情况下,您只需要包含 boost/serialization 目录中的相应标头即可。

对于尚无序列化标头的类型,您首先需要实现序列化代码,然后才能使用 Boost.MPI 传输类型。考虑一个简单的类 gps_position,其中包含成员 degreesminutesseconds。通过使其成为 boost::serialization::access 的友元并引入模板化的 serialize() 函数,使此类可序列化,如下所示:

class gps_position
{
private:
    friend class boost::serialization::access;

    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & degrees;
        ar & minutes;
        ar & seconds;
    }

    int degrees;
    int minutes;
    float seconds;
public:
    gps_position(){};
    gps_position(int d, int m, float s) :
        degrees(d), minutes(m), seconds(s)
    {}
};

有关使类型可序列化的完整信息超出了本教程的范围。有关更多信息,请参阅从中提取上述示例的 Boost.Serialization 库教程。使类型可序列化以用于 Boost.MPI 的一个重要的附带好处是,它们变得可序列化以用于任何其他用途,例如将对象存储到磁盘并在 XML 中操作它们。

某些可序列化类型,如上面的 gps_position,在固定偏移量处存储了固定数量的数据,并且完全由其数据成员的值定义(大多数没有指针的 POD 就是一个很好的例子)。在这种情况下,Boost.MPI 可以通过避免不必要的复制操作来优化其序列化和传输。要启用此优化,用户必须专门化类型特征 is_mpi_datatype,例如

namespace boost { namespace mpi {
  template <>
  struct is_mpi_datatype<gps_position> : mpl::true_ { };
} }

对于非模板类型,我们定义了一个宏来简化将类型声明为 MPI 数据类型的操作

BOOST_IS_MPI_DATATYPE(gps_position)

对于复合特征,is_mpi_datatype 的专门化可能取决于 is_mpi_datatype 本身。例如,只有当它存储的参数类型固定时,boost::array 对象才是固定的

namespace boost { namespace mpi {
  template <typename T, std::size_t N>
  struct is_mpi_datatype<array<T, N> >
    : public is_mpi_datatype<T> { };
} }

只有当数据类型的形状完全固定时,才能应用冗余复制消除优化。可变长度类型(例如,字符串、链表)和存储指针的类型不能使用优化,但 Boost.MPI 将无法在编译时检测到此错误。尝试在不正确的情况下执行此优化可能会导致分段错误和其他奇怪的程序行为。

Boost.MPI 可以将任何用户定义的数据类型从一个进程传输到另一个进程。内置类型可以毫不费力地传输;库定义的类型需要包含序列化标头;用户定义的类型将需要添加序列化代码。可以使用 is_mpi_datatype 类型特征优化固定数据类型的传输。

与 Boost.MPI 的通信始终通过通信器进行。通信器包含一组进程,这些进程可以在彼此之间发送消息并执行集体操作。一个程序中可以有多个通信器,每个通信器都包含其自身隔离的通信空间,该空间独立于其他通信器运行。

当 MPI 环境初始化时,只有“world”通信器(在 MPI C 和 Fortran 绑定中称为 MPI_COMM_WORLD)可用。“world”通信器通过默认构造 mpi::communicator 对象访问,其中包含程序开始执行时存在的所有 MPI 进程。然后可以通过复制或构建“world”通信器的子集来构造其他通信器。例如,在以下程序中,我们将进程拆分为两组:一组用于生成数据的进程,另一组用于收集数据的进程。 (generate_collect.cpp)

#include <boost/mpi.hpp>
#include <iostream>
#include <cstdlib>
#include <boost/serialization/vector.hpp>
namespace mpi = boost::mpi;

enum message_tags {msg_data_packet, msg_broadcast_data, msg_finished};

void generate_data(mpi::communicator local, mpi::communicator world);
void collect_data(mpi::communicator local, mpi::communicator world);

int main()
{
  mpi::environment env;
  mpi::communicator world;

  bool is_generator = world.rank() < 2 * world.size() / 3;
  mpi::communicator local = world.split(is_generator? 0 : 1);
  if (is_generator) generate_data(local, world);
  else collect_data(local, world);

  return 0;
}

当以这种方式拆分通信器时,它们的进程保留在原始通信器(拆分不会更改原始通信器)和新通信器中的成员身份。但是,进程的等级可能因通信器而异,因为通信器内的等级值始终是以零开头的连续值。在上面的示例中,前三分之二的进程成为“生成器”,其余进程成为“收集器”。“收集器”在 world 通信器中的等级将为 2/3 world.size() 或更大,而同一收集器进程在 local 通信器中的等级将从零开始。以下摘录自 collect_data() (在 generate_collect.cpp 中) 说明了如何管理多个通信器

mpi::status msg = world.probe();
if (msg.tag() == msg_data_packet) {
  // Receive the packet of data
  std::vector<int> data;
  world.recv(msg.source(), msg.tag(), data);

  // Tell each of the collectors that we'll be broadcasting some data
  for (int dest = 1; dest < local.size(); ++dest)
    local.send(dest, msg_broadcast_data, msg.source());

  // Broadcast the actual data.
  broadcast(local, data, 0);
}

此例外中的代码由“master”收集器执行,例如,在 world 通信器中等级为 2/3 world.size() 且在 local (收集器)通信器中等级为 0 的节点。它通过 world 通信器接收来自生成器的消息,然后通过 local 通信器将消息广播给每个收集器。

为了更好地控制进程子组的通信器的创建,Boost.MPI group 提供了计算两个组的并集 (|)、交集 (&) 和差集 (-)、生成任意子组等功能。

可以将通信器组织为笛卡尔网格,这是一个基本示例

#include <vector>
#include <iostream>

#include <boost/mpi/communicator.hpp>
#include <boost/mpi/collectives.hpp>
#include <boost/mpi/environment.hpp>
#include <boost/mpi/cartesian_communicator.hpp>

#include <boost/test/minimal.hpp>

namespace mpi = boost::mpi;
int test_main(int argc, char* argv[])
{
  mpi::environment  env;
  mpi::communicator world;

  if (world.size() != 24)  return -1;
  mpi::cartesian_dimension dims[] = {{2, true}, {3,true}, {4,true}};
  mpi::cartesian_communicator cart(world, mpi::cartesian_topology(dims));
  for (int r = 0; r < cart.size(); ++r) {
    cart.barrier();
    if (r == cart.rank()) {
      std::vector<int> c = cart.coordinates(r);
      std::cout << "rk :" << r << " coords: "
                << c[0] << ' ' << c[1] << ' ' << c[2] << '\n';
    }
  }
  return 0;
}

混合了分布式和共享内存并行性的混合并行应用程序越来越多。要了解如何支持该模型,需要知道 MPI 实现保证的线程支持级别。 mpi::threading::level 描述了 4 个有序的可能的线程支持级别。在最低级别,您根本不应使用线程,在最高级别,任何线程都可以执行 MPI 调用。

如果要在 MPI 应用程序中使用多线程,则应在环境构造函数中指示您首选的线程支持。然后探测库提供的线程支持,并决定如何使用它(可能什么都不做,那么中止是一个有效的选择)

#include <boost/mpi/environment.hpp>
#include <boost/mpi/communicator.hpp>
#include <iostream>
namespace mpi = boost::mpi;
namespace mt  = mpi::threading;

int main()
{
  mpi::environment env(mt::funneled);
  if (env.thread_level() < mt::funneled) {
     env.abort(-1);
  }
  mpi::communicator world;
  std::cout << "I am process " << world.rank() << " of " << world.size()
            << "." << std::endl;
  return 0;
}

当通过 MPI 传输非 MPI 基本的数据类型(例如字符串、列表和用户定义的数据类型)时,Boost.MPI 必须首先将这些数据类型序列化到缓冲区中,然后再进行通信;接收者然后将结果复制到缓冲区中,然后再反序列化为另一端的对象。对于某些数据类型,可以通过使用 is_mpi_datatype 来消除此开销。但是,可变长度数据类型(例如字符串和列表)不能是 MPI 数据类型。

Boost.MPI 支持第二种技术来提高性能,即将这些可变长度数据结构的结构与数据结构中存储的内容分离。仅当数据结构的形状保持不变但数据结构的内容需要多次通信时,此功能才有利。例如,在有限元分析中,网格的结构可能在计算开始时是固定的,但网格单元上的各种变量(温度、应力等)将在迭代分析过程中多次通信。在这种情况下,Boost.MPI 允许首先发送一次网格的“骨架”,然后多次传输“内容”。由于内容不需要包含有关数据类型结构的任何信息,因此可以传输内容而无需创建单独的通信缓冲区。

为了说明骨架和内容的使用,我们将采用一个稍微有限的示例,其中主进程将随机数字序列生成到列表中,并将它们传输到多个从属进程。列表的长度将在程序启动时固定,因此可以有效地传输列表的内容(即,当前数字序列)。完整的示例位于 example/random_content.cpp 中。我们从主进程(等级 0)开始,该进程构建一个列表,通过 skeleton 传达其结构,然后重复生成要通过 content 广播到从属进程的随机数字序列

// Generate the list and broadcast its structure
std::list<int> l(list_len);
broadcast(world, mpi::skeleton(l), 0);

// Generate content several times and broadcast out that content
mpi::content c = mpi::get_content(l);
for (int i = 0; i < iterations; ++i) {
  // Generate new random values
  std::generate(l.begin(), l.end(), &random);

  // Broadcast the new content of l
  broadcast(world, c, 0);
}

// Notify the slaves that we're done by sending all zeroes
std::fill(l.begin(), l.end(), 0);
broadcast(world, c, 0);

从属进程的结构与主进程非常相似。它们接收(通过 broadcast() 调用)数据结构的骨架,然后使用它来构建自己的整数列表。在每次迭代中,它们通过另一个 broadcast() 接收数据结构中的新内容,并计算数据的某些属性

// Receive the content and build up our own list
std::list<int> l;
broadcast(world, mpi::skeleton(l), 0);

mpi::content c = mpi::get_content(l);
int i = 0;
do {
  broadcast(world, c, 0);

  if (std::find_if
       (l.begin(), l.end(),
        std::bind1st(std::not_equal_to<int>(), 0)) == l.end())
    break;

  // Compute some property of the data.

  ++i;
} while (true);

任何可序列化数据类型的骨架和内容都可以通过 sendrecv 成员(对于点对点通信器)或通过 broadcast() 集体通信进行传输。当将数据结构分离为骨架和内容时,请注意不要在未重新传输骨架的情况下修改数据结构(无论是在发送方还是接收方)。 Boost.MPI 无法检测到对数据结构的这些意外修改,这很可能导致传输不正确的数据或程序不稳定。

为了获得不包含任何指针的小型定长数据类型的最佳性能,非常重要的是使用 Boost.MPI 和 Boost.Serialization 的类型特征来标记它们。

前面已经讨论过,不包含指针的定长类型可以用作 is_mpi_datatype,例如:

namespace boost { namespace mpi {
  template <>
  struct is_mpi_datatype<gps_position> : mpl::true_ { };
} }

或等效的宏

BOOST_IS_MPI_DATATYPE(gps_position)

此外,如果未使用指向这些类型的指针,则通过使用 Boost.Serialization 的 traits 类或辅助宏,可以显著提高性能,关闭对这些类型的追踪和版本控制

BOOST_CLASS_TRACKING(gps_position,track_never)
BOOST_CLASS_IMPLEMENTATION(gps_position,object_serializable)

在同构机器上可以进行更多优化,方法是避免 MPI_Pack/MPI_Unpack 调用,而是使用直接按位复制。默认情况下,通过在包含文件 boost/mpi/config.hpp 中定义宏 BOOST_MPI_HOMOGENEOUS 来启用此功能。在构建 Boost.MPI 和构建应用程序时,该定义必须保持一致。

此外,所有类都需要同时标记为 is_mpi_datatype 和 is_bitwise_serializable,通过使用 Boost.Serialization 的辅助宏

BOOST_IS_BITWISE_SERIALIZABLE(gps_position)

通常,对于 is_mpi_datatype 为 true 的类,使用位数据的二进制复制进行序列化是安全的。例外情况是某些成员应该跳过序列化的类。



[10] 根据 MPI 标准,初始化必须在 main 函数被调用后由用户主动进行。


PrevUpHomeNext