Hao Yu's blog

The program monkey was eaten by the siege lion.

0%

Gloo 是一个集合通信库。它带有许多对机器学习应用有用的集体算法。参与机器之间的数据传输是抽象的,因此可以随时使用 IP,或者在可用时使用 InifiniBand(或 RoCE)。 在后一种情况下,如果使用 InfiniBand 传输,GPUDirect可用于加速跨机器 GPU 到 GPU 的内存传输。

在适用的情况下,算法具有一种适用于系统内存缓冲区的实现,以及一种适用于 NVIDIA GPU 内存缓冲区的实现。 在后一种情况下,主机和设备之间不需要复制内存; 这是由算法实现处理的。

aligned_allocator

将分配的内存区域对齐到32字节。使用了using和模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Align buffers to 32 bytes to support vectorized code
const size_t kBufferAlignment = 32;

template <typename T, int ALIGNMENT = kBufferAlignment>
class aligned_allocator {
static_assert(
!(ALIGNMENT & (ALIGNMENT - 1)),
"alignment must be a power of 2");

public:
using value_type = T;
using pointer = value_type*;
using const_pointer = const value_type*;
using reference = value_type&;
using const_reference = const value_type&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;

template <typename U>
struct rebind {
using other = aligned_allocator<U, ALIGNMENT>;
};

inline explicit aligned_allocator() = default;
inline ~aligned_allocator() = default;
inline explicit aligned_allocator(const aligned_allocator& a) = default;

inline pointer address(reference r) {
return &r;
}

inline const_pointer address(const_reference r) {
return &r;
}

inline pointer allocate(size_type sz) {
pointer p;
if (posix_memalign(
reinterpret_cast<void**>(&p), ALIGNMENT, sizeof(T) * sz)) {
abort();
}
return p;
}

void deallocate(pointer p, size_type /*sz*/) {
free(p);
}
};

调用posix_memalign(void **memptr, size_t alignment, size_t size)成功时会返回size字节的动态内存,并且这块内存的地址是alignment的倍数。参数alignment必须是2的幂,还是void指针的大小的倍数。返回的内存块的地址放在了memptr里面,函数返回值是0。调用失败时,没有内存会被分配,memptr的值没有被定义,返回如下错误码之一:

  • EINVAL:参数不是2的幂,或者不是void指针的倍数。
  • ENOMEM:没有足够的内存去满足函数的请求。

transport

一个样例是这样调用mpi的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int /*argc*/, char** /*argv*/) {
// We'll use the TCP transport in this example
auto dev = gloo::transport::tcp::CreateDevice("localhost");

// Create Gloo context and delegate management of MPI_Init/MPI_Finalize
auto context = gloo::mpi::Context::createManaged();
context->connectFullMesh(dev);

// Create and run simple allreduce
int rank = context->rank;
gloo::AllreduceRing<int> allreduce(context, {&rank}, 1);
allreduce.run();
std::cout << "Result: " << rank << std::endl;

return 0;
}

transport共有三种通信方式:ibverbs、tcp、uv。

ibverbs

应该是RDMA方式。context应该是上下文,记录通信设备和rank相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace ibverbs {

Context::Context(std::shared_ptr<Device> device, int rank, int size)
: ::gloo::transport::Context(rank, size), device_(device) {}

Context::~Context() {}

std::unique_ptr<transport::Pair>& Context::createPair(int rank) {
pairs_[rank] = std::unique_ptr<transport::Pair>(
new ibverbs::Pair(device_, getTimeout()));
return pairs_[rank];
}

std::unique_ptr<transport::UnboundBuffer> Context::createUnboundBuffer(
void* ptr,
size_t size) {
GLOO_THROW_INVALID_OPERATION_EXCEPTION(
"Unbound buffers not supported yet for ibverbs transport");
return std::unique_ptr<transport::UnboundBuffer>();
}

} // namespace ibverbs

这个pair是不能复制和赋值,避免出现内存问题,感觉应该是通信双方组成一个pair:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Forward declaration
class Buffer;

class Pair : public ::gloo::transport::Pair {
static constexpr int kMaxBuffers = 8;
static constexpr auto kRecvCompletionQueueCapacity = kMaxBuffers;
static constexpr auto kSendCompletionQueueCapacity = kMaxBuffers;
static constexpr auto kCompletionQueueCapacity =
kRecvCompletionQueueCapacity + kSendCompletionQueueCapacity;

// The ibv_req_notify(3) function takes an argument called
// 'solicited_only' which makes it only trigger a notification for
// work requests that are flagged as solicited. Every completion
// should trigger a notification, so always pass 0.
static constexpr auto kNotifyOnAnyCompletion = 0;

public:
explicit Pair(
const std::shared_ptr<Device>& dev,
std::chrono::milliseconds timeout);

virtual ~Pair();

Pair(const Pair& that) = delete;

Pair& operator=(const Pair& that) = delete;

virtual const Address& address() const override;
virtual void connect(const std::vector<char>& bytes) override;
virtual void setSync(bool enable, bool busyPoll) override;

virtual std::unique_ptr<::gloo::transport::Buffer>
createSendBuffer(int slot, void* ptr, size_t size) override;

virtual std::unique_ptr<::gloo::transport::Buffer>
createRecvBuffer(int slot, void* ptr, size_t size) override;

// Send from the specified buffer to remote side of pair.
virtual void send(
transport::UnboundBuffer* tbuf,
uint64_t tag,
size_t offset,
size_t nbytes) override;

// Receive into the specified buffer from the remote side of pair.
virtual void recv(
transport::UnboundBuffer* tbuf,
uint64_t tag,
size_t offset,
size_t nbytes) override;

void handleCompletionEvent();

void pollCompletions();

void handleCompletion(struct ibv_wc* wc);

void send(Buffer* buf, size_t offset, size_t length, size_t roffset);

void close() override;

protected:
std::shared_ptr<Device> dev_;

// Whether or not this pair is running in sync mode.
std::atomic<bool> sync_;

// Whether or not this pair is busy polling in sync mode.
std::atomic<bool> busyPoll_;

const std::chrono::milliseconds timeout_;

// 该pair的完成队列处理的完成事件数。在销毁完成队列之前,需要确认这么多事件。否则,销毁将挂起。
int completionEventsHandled_;

Address self_;
Address peer_;

struct ibv_cq* cq_;
struct ibv_qp* qp_;

std::mutex m_;
std::condition_variable cv_;

// For us to copy the remote peer's ibv_mr into.
std::map<int, struct ibv_mr> peerMemoryRegions_;

// 这些字段存储pair的远程端可以发送到的内存区域以及pair的本地端可以从中发送的内存区域。
// 注册接收缓冲区时,本地 ibv_mr 被发送到pair的远程端,并且相应的 MemoryRegion 实例保留在 mappedSendRegions_ 列表中,直到发送操作完成。
// 为了允许pair的远程端发送其内存区域,我们在 mappedRecvRegions_ 中保留了固定数量的 MemoryRegion 实例。
// 对于每个发布的接收工作请求,这些区域都会被循环引用。
std::map<int, std::unique_ptr<MemoryRegion> > mappedSendRegions_;
std::array<std::unique_ptr<MemoryRegion>, kMaxBuffers> mappedRecvRegions_;

// 跟踪发布和完成的请求工作请求的数量。 在发布 WR 和完成 WR 时,都需要对 mappedRecvRegions_ 数组进行索引。
uint64_t recvPosted_;

// Completions on behalf of buffers need to be forwarded to those buffers.
std::map<int, Buffer*> sendCompletionHandlers_;
std::map<int, Buffer*> recvCompletionHandlers_;

void sendMemoryRegion(struct ibv_mr* mr, int slot);
const struct ibv_mr* getMemoryRegion(int slot);

void postReceive();

std::chrono::milliseconds getTimeout() const {
return timeout_;
}

const Address& peer() const {
return peer_;
}

private:
std::exception_ptr ex_;
bool closed_ = false;

// Used to signal IO exceptions from one thread and propagate onto others.
void signalIoFailure(const std::string& msg);
void checkErrorState();

friend class Buffer;
};

以下是逐个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
Pair::Pair(
const std::shared_ptr<Device>& dev,
std::chrono::milliseconds timeout)
: dev_(dev),
sync_(false),
busyPoll_(false),
timeout_(timeout),
completionEventsHandled_(0),
recvPosted_(0),
ex_(nullptr) {
int rv;

// Create completion queue
{
// 必须向设备的完成通道注册此完成队列以支持异步完成处理。
// Pairs 默认使用异步完成处理,因此我们调用 ibv_req_notify_cq(3) 来请求第一个通知。
cq_ = ibv_create_cq(
dev_->context_,
kCompletionQueueCapacity,
this,
dev_->comp_channel_,
0);
GLOO_ENFORCE(cq_);

// 在完成队列 (CQ) 上请求Completion Notification(完成通知)。
rv = ibv_req_notify_cq(cq_, kNotifyOnAnyCompletion);
GLOO_ENFORCE_EQ(rv, 0);
}

// Create queue pair
{
struct ibv_qp_init_attr attr;
memset(&attr, 0, sizeof(struct ibv_qp_init_attr));
attr.send_cq = cq_;
attr.recv_cq = cq_;
attr.cap.max_send_wr = Pair::kSendCompletionQueueCapacity;
attr.cap.max_recv_wr = Pair::kRecvCompletionQueueCapacity;
attr.cap.max_send_sge = 1;
attr.cap.max_recv_sge = 1;
attr.qp_type = IBV_QPT_RC;
qp_ = ibv_create_qp(dev->pd_, &attr);
// 创建queue pair
GLOO_ENFORCE(qp_);
}

// Init queue pair
{
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(struct ibv_qp_attr));
attr.qp_state = IBV_QPS_INIT;
attr.pkey_index = 0;
attr.port_num = dev_->attr_.port;
attr.qp_access_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE;
rv = ibv_modify_qp(
qp_,
&attr,
IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS);
GLOO_ENFORCE_EQ(rv, 0);
}

// Populate local address.
// The Packet Sequence Number field (PSN) is random which makes that
// the remote end of this pair needs to have the contents of the
// full address struct in order to connect, and vice versa.
{
struct ibv_port_attr attr;
memset(&attr, 0, sizeof(struct ibv_port_attr));
rv = ibv_query_port(dev_->context_, dev_->attr_.port, &attr);
GLOO_ENFORCE_EQ(rv, 0);
rv = ibv_query_gid(
dev_->context_,
dev_->attr_.port,
dev_->attr_.index,
&self_.addr_.ibv_gid);
GLOO_ENFORCE_EQ(rv, 0);
self_.addr_.lid = attr.lid;
self_.addr_.qpn = qp_->qp_num;
self_.addr_.psn = rand() & 0xffffff;
}

// 在连接之前发布接收请求。
// 每当这pair的远程端注册接收缓冲区时,就会触发它们的内存注册被发送到这一端。
// 由于这些发送是单方面的,我们总是需要一整套接收工作请求。
// 内存区域接收可以与常规缓冲区写入交错,因此我们主动在每个接收工作请求中包含一个内存区域。
for (int i = 0; i < kMaxBuffers; ++i) {
mappedRecvRegions_[i] = make_unique<MemoryRegion>(dev_->pd_);
postReceive();
}
}

Pair::~Pair() {
int rv;

// Acknowledge number of completion events handled by this
// pair's completion queue (also see ibv_get_cq_event(3)).
ibv_ack_cq_events(cq_, completionEventsHandled_);

rv = ibv_destroy_qp(qp_);
GLOO_ENFORCE_EQ(rv, 0);

rv = ibv_destroy_cq(cq_);
GLOO_ENFORCE_EQ(rv, 0);
}

void Pair::close() {
if (closed_) {
// TODO: add proper handling of duplicate closes T21171834
return;
}

closed_ = true;
}

const Address& Pair::address() const {
return self_;
}

连接函数先获取到对方的地址,更新attr结构体,使用ibv_modify_qp函数修改RDMA通信所需的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void Pair::connect(const std::vector<char>& bytes) {
struct ibv_qp_attr attr;
int rv;
checkErrorState();

peer_ = Address(bytes);

memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_1024;
attr.dest_qp_num = peer_.addr_.qpn;
attr.rq_psn = peer_.addr_.psn;
attr.max_dest_rd_atomic = 1;
attr.min_rnr_timer = 20;
attr.ah_attr.is_global = 0;
attr.ah_attr.dlid = peer_.addr_.lid;
attr.ah_attr.port_num = dev_->attr_.port;
if (peer_.addr_.ibv_gid.global.interface_id) {
attr.ah_attr.is_global = 1;
attr.ah_attr.grh.hop_limit = 1;
attr.ah_attr.grh.dgid = peer_.addr_.ibv_gid;
attr.ah_attr.grh.sgid_index = dev_->attr_.index;
}

// ibv_modify_qp()修改队列对的属性。更改的属性描述了QP的发送和接收属性。
// ibv_create_qp仅仅分配了资源,要通过这个modify来让硬件进入工作状态。
// Move to Ready To Receive (RTR) state
rv = ibv_modify_qp(
qp_,
&attr,
IBV_QP_STATE | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN |
IBV_QP_AV | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER);
GLOO_ENFORCE_EQ(rv, 0);

memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTS;
attr.sq_psn = self_.addr_.psn;
attr.ah_attr.is_global = 1;
attr.timeout = 14;
attr.retry_cnt = 7;
attr.rnr_retry = 7; /* infinite */
attr.max_rd_atomic = 1;

// Move to Ready To Send (RTS) state
rv = ibv_modify_qp(
qp_,
&attr,
IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY |
IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC);
GLOO_ENFORCE_EQ(rv, 0);
}

// Switches the pair into synchronous mode.
//
// Note: busy polling is NOT optional. Currently, since all pairs
// share a single completion channel, busy polling is mandatory
// through ibv_poll_cq(3). If a use case comes up for supporting
// synchronous mode where the calling thread should be suspended, this
// can be revisited and we can add a completion channel per pair.
//
void Pair::setSync(bool sync, bool busyPoll) {
checkErrorState();
if (!sync) {
GLOO_THROW_INVALID_OPERATION_EXCEPTION("Can only switch to sync mode");
}
if (!busyPoll) {
GLOO_THROW_INVALID_OPERATION_EXCEPTION(
"The ibverbs transport only supports busy polling in sync mode");
}

// The notification mechanism for this pair's completion queue is
// still armed. This means the device thread will still call
// handleCompletions() one more time, but this is ignored.
//
// No need to lock a mutex; these are atomics.
//
sync_ = true;
busyPoll_ = true;
}

使用ibv_post_send函数发送,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Pair::sendMemoryRegion(struct ibv_mr* src, int slot) {
auto mr = make_unique<MemoryRegion>(dev_->pd_, src);
struct ibv_sge list = mr->sge();
struct ibv_send_wr wr;
memset(&wr, 0, sizeof(wr));
wr.wr_id = slot;
wr.sg_list = &list;
wr.num_sge = 1;
wr.opcode = IBV_WR_SEND_WITH_IMM;
wr.send_flags = IBV_SEND_SIGNALED;
wr.imm_data = slot;

// 工作请求被序列化并发送到驱动程序,因此它不需要在 ibv_post_send 调用后有效。
// ibv_post_send和recv用于发送verb,verb承载在一个称为ibv_send_wr或者ibv_recv_wr的数据结构中,里面是verb类型和mr的相关细节。
struct ibv_send_wr* bad_wr = nullptr;
int rv = ibv_post_send(qp_, &wr, &bad_wr);
if (rv != 0) {
signalIoFailure(GLOO_ERROR_MSG("ibv_post_send: ", rv));
}

GLOO_ENFORCE_EQ(mappedSendRegions_.count(slot), 0);
mappedSendRegions_[slot] = std::move(mr);
}

先获取到锁,如果是异步的,需要检查是不是超时了;否则就等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const struct ibv_mr* Pair::getMemoryRegion(int slot) {
std::unique_lock<std::mutex> lock(m_);
if (sync_) {
auto it = peerMemoryRegions_.find(slot);
auto start = std::chrono::steady_clock::now();
while (it == peerMemoryRegions_.end()) {
lock.unlock();
pollCompletions();
lock.lock();
if (timeout_ != kNoTimeout &&
(std::chrono::steady_clock::now() - start) >= timeout_) {
lock.unlock();
signalIoFailure(
GLOO_ERROR_MSG(
"Timeout waiting for memory region from ",
peer_.str()));
GLOO_ENFORCE(false, "Unexpected code path");
}
it = peerMemoryRegions_.find(slot);
}
return &it->second;
} else {
auto pred = [&]{
return peerMemoryRegions_.find(slot) != peerMemoryRegions_.end();
};
if (timeout_ == kNoTimeout) {
// No timeout set. Wait for read to complete.
cv_.wait(lock, pred);
} else {
auto done = cv_.wait_for(lock, timeout_, pred);
if (!done) {
signalIoFailure(
GLOO_ERROR_MSG(
"Timeout waiting for memory region from ",
peer_.str()));
GLOO_ENFORCE(false, "Unexpected code path");
}
}
auto it = peerMemoryRegions_.find(slot);
GLOO_ENFORCE(it != peerMemoryRegions_.end());
return &it->second;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
void Pair::postReceive() {
const auto& mr = mappedRecvRegions_[recvPosted_++ % kMaxBuffers];
struct ibv_sge list = mr->sge();
struct ibv_recv_wr wr;
memset(&wr, 0, sizeof(wr));
wr.sg_list = &list;
wr.num_sge = 1;

// 工作请求被序列化并发送到驱动程序,因此它不需要在 ibv_post_recv 调用后有效。
struct ibv_recv_wr* bad_wr = nullptr;
auto rv = ibv_post_recv(qp_, &wr, &bad_wr);
if (rv != 0) {
signalIoFailure(GLOO_ERROR_MSG("ibv_post_recv: ", rv));
}
}

std::unique_ptr<::gloo::transport::Buffer>
Pair::createSendBuffer(int slot, void* ptr, size_t size) {
// 创建一个buffer
std::unique_lock<std::mutex> lock(m_);
GLOO_ENFORCE_EQ(sendCompletionHandlers_.count(slot), 0);
auto buffer = new Buffer(this, slot, ptr, size);
sendCompletionHandlers_[slot] = buffer;
return std::unique_ptr<::gloo::transport::Buffer>(buffer);
}

std::unique_ptr<::gloo::transport::Buffer>
Pair::createRecvBuffer(int slot, void* ptr, size_t size) {
std::unique_lock<std::mutex> lock(m_);
GLOO_ENFORCE_EQ(recvCompletionHandlers_.count(slot), 0);
auto buffer = new Buffer(this, slot, ptr, size);
recvCompletionHandlers_[slot] = buffer;
sendMemoryRegion(buffer->mr_, buffer->slot_);
return std::unique_ptr<::gloo::transport::Buffer>(buffer);
}

// Send from the specified buffer to remote side of pair.
void Pair::send(
transport::UnboundBuffer* tbuf,
uint64_t /* unused */,
size_t /* unused */,
size_t /* unused */) {
GLOO_THROW_INVALID_OPERATION_EXCEPTION(
"Unbound buffers not supported yet for ibverbs transport");
}

// Receive into the specified buffer from the remote side of pair.
void Pair::recv(
transport::UnboundBuffer* tbuf,
uint64_t /* unused */,
size_t /* unused */,
size_t /* unused */) {
GLOO_THROW_INVALID_OPERATION_EXCEPTION(
"Unbound buffers not supported yet for ibverbs transport");
}

// handleCompletionEvent is called by the device thread when it
// received an event for this pair's completion queue on its
// completion channel.
void Pair::handleCompletionEvent() {
int rv;

completionEventsHandled_++;

// If in sync mode, the pair was just switched and this is
// the last notification from the device thread because
// the notification mechanism is not re-armed below.
if (sync_) {
return;
}

try {
checkErrorState();

// Arm notification mechanism for completion queue.
rv = ibv_req_notify_cq(cq_, kNotifyOnAnyCompletion);
GLOO_ENFORCE_EQ(rv, 0);

// Now poll for work completions to drain the completion queue.
std::unique_lock<std::mutex> lock(m_);
pollCompletions();
} catch (const ::gloo::IoException&) {
// Catch IO exceptions on the event handling thread. The exception has
// already been saved and user threads signaled.
}
}

轮询这pair的完成队列以获取工作完成情况。当从设备线程调用时,这对的互斥锁已经被获取。从用户线程调用时,不会获取互斥锁(因为只有一个线程使用这对)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
void Pair::pollCompletions() {
std::array<struct ibv_wc, kCompletionQueueCapacity> wc;

// Invoke handler for every work completion.
for (;;) {
auto nwc = ibv_poll_cq(cq_, wc.size(), wc.data());
GLOO_ENFORCE_GE(nwc, 0);

// Handle work completions
for (int i = 0; i < nwc; i++) {
checkErrorState();
handleCompletion(&wc[i]);
}

// Break unless wc was filled
if (nwc == 0 || nwc < wc.size()) {
break;
}
}
}

void Pair::handleCompletion(struct ibv_wc* wc) {
if (wc->opcode == IBV_WC_RECV_RDMA_WITH_IMM) {
// Incoming RDMA write completed.
// Slot is encoded in immediate data on receive work completion.
// It is set in the Pair::send function.
auto slot = wc->imm_data;
GLOO_ENFORCE_EQ( wc->status, IBV_WC_SUCCESS, "Recv for slot ", slot, ": ", ibv_wc_status_str(wc->status));

GLOO_ENFORCE(recvCompletionHandlers_[slot] != nullptr);
recvCompletionHandlers_[slot]->handleCompletion(wc);

// Backfill receive work requests.
postReceive();
} else if (wc->opcode == IBV_WC_RDMA_WRITE) {
// Outbound RDMA write completed.
// Slot is encoded in wr_id fields on send work request. Unlike
// the receive work completions, the immediate data field on send
// work requests are not pass to the respective work completion.
auto slot = wc->wr_id;
GLOO_ENFORCE_EQ( wc->status, IBV_WC_SUCCESS, "Send for slot ", slot, ": ", ibv_wc_status_str(wc->status));

GLOO_ENFORCE(sendCompletionHandlers_[slot] != nullptr);
sendCompletionHandlers_[slot]->handleCompletion(wc);
} else if (wc->opcode == IBV_WC_RECV) {
// 内存区域 recv 完成。
// 仅由pair的远程端用于传递 ibv_mr。
// 它们以 FIFO 顺序写入,因此我们可以在映射的接收区域列表中选择并使用第一个 MemoryRegion 实例。
// 尝试写入此插槽的缓冲区可能正在等待该对的另一端发送其内存区域。
// 锁定访问权限,然后通知任何等待的人。
// 时隙在接收工作完成后立即编码为数据。 它在 Pair::sendMemoryRegion 函数中设置。
auto slot = wc->imm_data;
GLOO_ENFORCE_EQ(
wc->status,
IBV_WC_SUCCESS,
"Memory region recv for slot ",
slot,
": ",
ibv_wc_status_str(wc->status));

// Move ibv_mr from memory region 'inbox' to final slot.
const auto& mr = mappedRecvRegions_[recvPosted_ % kMaxBuffers];
peerMemoryRegions_[slot] = mr->mr();

// Notify any buffer waiting for the details of its remote peer.
cv_.notify_all();

// Backfill receive work requests.
postReceive();
} else if (wc->opcode == IBV_WC_SEND) {
// Memory region send completed.
auto slot = wc->wr_id;
GLOO_ENFORCE_EQ(
wc->status,
IBV_WC_SUCCESS,
"Memory region send for slot ",
slot,
": ",
ibv_wc_status_str(wc->status));

GLOO_ENFORCE_GT(mappedSendRegions_.size(), 0);
GLOO_ENFORCE_EQ(mappedSendRegions_.count(slot), 1);
mappedSendRegions_.erase(slot);
} else {
GLOO_ENFORCE(false, "Unexpected completion with opcode: ", wc->opcode);
}
}

使用一些信息填充结构体,获取到内存区域后发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void Pair::send(Buffer* buffer, size_t offset, size_t length, size_t roffset) {
struct ibv_sge list;
list.addr = (uint64_t)buffer->ptr_ + offset;
list.length = length;
list.lkey = buffer->mr_->lkey;

struct ibv_send_wr wr;
memset(&wr, 0, sizeof(wr));
wr.wr_id = buffer->slot_;
wr.sg_list = &list;
wr.num_sge = 1;
wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM;
wr.send_flags = IBV_SEND_SIGNALED;
wr.imm_data = buffer->slot_;

const struct ibv_mr* peer = getMemoryRegion(buffer->slot_);
GLOO_ENFORCE_NE(peer, (const struct ibv_mr*)nullptr);
wr.wr.rdma.remote_addr = (uint64_t)peer->addr + roffset;
wr.wr.rdma.rkey = peer->rkey;

struct ibv_send_wr* bad_wr;
auto rv = ibv_post_send(qp_, &wr, &bad_wr);
if (rv != 0) {
signalIoFailure(GLOO_ERROR_MSG("ibv_post_send: ", rv));
}
}

void Pair::signalIoFailure(const std::string& msg) {
std::lock_guard<std::mutex> lock(m_);
auto ex = ::gloo::IoException(msg);
if (ex_ == nullptr) {
// If we haven't seen an error yet, store the exception to throw on future calling threads.
ex_ = std::make_exception_ptr(ex);
// Loop through the completion handlers and signal that an error has
// occurred.
for (auto& it : recvCompletionHandlers_) {
GLOO_ENFORCE(it.second != nullptr);
it.second->signalError(ex_);
}
for (auto& it : sendCompletionHandlers_) {
GLOO_ENFORCE(it.second != nullptr);
it.second->signalError(ex_);
}
}
// Finally, throw the exception on this thread.
throw ex;
};

void Pair::checkErrorState() {
// If we previously encountered an error, rethrow here.
if (ex_ != nullptr) {
std::rethrow_exception(ex_);
}
}

device 保存了设备信息,以下是构建一个设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Device::Device(const struct attr& attr, ibv_context* context)
: attr_(attr),
pciBusID_(infinibandToBusID(attr.name)),
hasNvPeerMem_(kernelModules().count("nv_peer_mem") > 0),
context_(context) {
int rv;

// Query and store device attributes
rv = ibv_query_device(context_, &deviceAttr_);
GLOO_ENFORCE_EQ(rv, 0, "ibv_query_device: ", strerror(errno));

// Query and store port attributes
rv = ibv_query_port(context_, attr_.port, &portAttr_);
GLOO_ENFORCE_EQ(rv, 0, "ibv_query_port: ", strerror(errno));

// Protection domain
pd_ = ibv_alloc_pd(context_);
GLOO_ENFORCE(pd_);

// Completion channel
comp_channel_ = ibv_create_comp_channel(context_);
GLOO_ENFORCE(comp_channel_);

// Start thread to poll completion queue and dispatch
// completions for completed work requests.
done_ = false;
loop_.reset(new std::thread(&Device::loop, this));
}

buffer分配一个缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Buffer::Buffer(Pair* pair, int slot, void* ptr, size_t size)
: ::gloo::transport::Buffer(slot, ptr, size),
pair_(pair),
recvCompletions_(0),
sendCompletions_(0),
sendPending_(0),
ex_(nullptr) {
// 注册一个memory region
mr_ = ibv_reg_mr(
pair_->dev_->pd_,
ptr_,
size_,
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);

// Provide hint if the error is EFAULT and nv_peer_mem is not loaded
if (mr_ == nullptr && errno == EFAULT) {
if (!pair->dev_->hasNvPeerMem_) {
GLOO_ENFORCE(
mr_ != nullptr,
"ibv_reg_mr: ",
strerror(errno),
" (kernel module 'nv_peer_mem' not loaded;"
" did you specify a pointer to GPU memory?)");
}
}

// Provide hint if the error is ENOMEM
if (mr_ == nullptr && errno == ENOMEM) {
GLOO_ENFORCE(
mr_ != nullptr,
"ibv_reg_mr: ",
strerror(errno),
" (did you run into the locked memory limit?)");
}

GLOO_ENFORCE(mr_ != nullptr, "ibv_reg_mr: ", strerror(errno));
}

等待接收操作完成。根据是不是异步判断是否需要等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void Buffer::waitRecv() {
// 如果该pair处于同步模式,则当前线程负责轮询工作完成情况。
// 由于单个pair可能为多个缓冲区提供服务,因此完成可能旨在用于另一个缓冲区。
auto timeout = pair_->getTimeout();
if (pair_->sync_) {
auto start = std::chrono::steady_clock::now();
// We can assume a single pair is never used by more than one
// thread, so there is no need to acquire the mutex here.
while (recvCompletions_ == 0) {
pair_->pollCompletions();
if (timeout != kNoTimeout &&
(std::chrono::steady_clock::now() - start) >= timeout) {
pair_->signalIoFailure(
GLOO_ERROR_MSG("Read timeout ", pair_->peer().str()));
GLOO_ENFORCE(false, "Unexpected code path");
}
}
recvCompletions_--;
} else {
// The device thread will signal completion. If the completion
// hasn't arrived yet, wait until it does.
auto pred = [&]{
checkErrorState();
return recvCompletions_ > 0;
};
std::unique_lock<std::mutex> lock(m_);
if (timeout == kNoTimeout) {
// No timeout set. Wait for read to complete.
recvCv_.wait(lock, pred);
} else {
auto done = recvCv_.wait_for(lock, timeout, pred);
if (!done) {
// Release the mutex before calling into the pair to avoid deadlock.
// Calling signalIoFailure() will throw, so no need to
// reacquire.
lock.unlock();
pair_->signalIoFailure(
GLOO_ERROR_MSG("Read timeout ", pair_->peer().str()));
GLOO_ENFORCE(false, "Unexpected code path");
}
}
recvCompletions_--;
}
}

等待发送操作完成。根据是不是异步判断是否需要等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Wait for the previous send operation to finish.
void Buffer::waitSend() {
// 如果该pair处于同步模式,则当前线程负责轮询工作完成情况。
auto timeout = pair_->getTimeout();
if (pair_->sync_) {
// We can assume a single pair is never used by more than one
// thread, so there is no need to acquire the mutex here.
if (sendCompletions_ == 0) {
GLOO_ENFORCE_GT(sendPending_, 0, "No send to wait for");
auto start = std::chrono::steady_clock::now();
// We can assume a single pair is never used by more than one
// thread, so there is no need to acquire the mutex here.
while (sendCompletions_ == 0) {
pair_->pollCompletions();
if (timeout != kNoTimeout &&
(std::chrono::steady_clock::now() - start) >= timeout) {
pair_->signalIoFailure(
GLOO_ERROR_MSG("Send timeout ", pair_->peer().str()));
GLOO_ENFORCE(false, "Unexpected code path");
}
}
}
sendCompletions_--;
} else {
// The device thread will signal completion. If the completion
// hasn't arrived yet, wait until it does.
std::unique_lock<std::mutex> lock(m_);
checkErrorState();
if (sendCompletions_ == 0) {
GLOO_ENFORCE_GT(sendPending_, 0, "No send to wait for");
auto pred = [&]{
checkErrorState();
return sendCompletions_ > 0;
};
if (timeout == kNoTimeout) {
// No timeout set. Wait for read to complete.
sendCv_.wait(lock, pred);
} else {
auto done = sendCv_.wait_for(lock, timeout, pred);
if (!done) {
// Release the mutex before calling into the pair to avoid deadlock.
// Calling signalIoFailure() will throw, so no need to
// reacquire.
lock.unlock();
pair_->signalIoFailure(
GLOO_ERROR_MSG("Send timeout ", pair_->peer().str()));
GLOO_ENFORCE(false, "Unexpected code path");
}
}
}
sendCompletions_--;
}
}

void Buffer::send(size_t offset, size_t length, size_t roffset) {
int rv;

// Can't assert on roffset, since we don't know the size of
// the remote buffer. Refactor of initialization code needed
// to support this.
GLOO_ENFORCE_LE(offset + length, size_);

{
std::unique_lock<std::mutex> lock(m_);
checkErrorState();
}

if (debug_) {
std::cout << "[" << getpid() << "] ";
std::cout << "send " << length << " bytes";
std::cout << std::endl;
}

// Increment number of sends in flight
sendPending_++;

pair_->send(this, offset, length, roffset);
}

tcp

TCP中包含一个tls层。TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。

tls

上下文:

1
2
3
4
5
6
7
8
9
Context::Context(std::shared_ptr<Device> device, int rank, int size)
: ::gloo::transport::tcp::Context(
std::dynamic_pointer_cast<::gloo::transport::tcp::Device>(device),
rank, size),
ssl_ctx_(create_ssl_ctx(c_str_or_null(device->getPKeyFile()),
c_str_or_null(device->getCertFile()),
c_str_or_null(device->getCAFile()),
c_str_or_null(device->getCAPath())),
[](::SSL_CTX *x) { ::_glootls::SSL_CTX_free(x); }) {}

真正创建ssl context的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
SSL_CTX *Context::create_ssl_ctx(const char *pkey, const char *cert,
const char *ca_file, const char *ca_path) {
GLOO_ENFORCE(pkey != nullptr && cert != nullptr,
"Private key and certificate location must be specified");
GLOO_ENFORCE(ca_file != nullptr || ca_path != nullptr,
"CAfile or CApath must be specified");
static std::once_flag ssl_ctx_init_;
std::call_once(ssl_ctx_init_, [] {
// SSL_load_error_strings();
// SSL_library_init();
_glootls::OPENSSL_init_ssl(
OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
_glootls::OPENSSL_init_ssl(0, NULL);
});
SSL_CTX *ssl_ctx = _glootls::SSL_CTX_new(_glootls::TLS_method());
GLOO_ENFORCE(ssl_ctx != nullptr, getSSLErrorMessage());
GLOO_ENFORCE(
_glootls::SSL_CTX_set_min_proto_version(ssl_ctx, TLS_MAX_VERSION) == 1,
getSSLErrorMessage());

// As we don't need to handle legacy clients,
// let's remove support for legacy renegotiation:
_glootls::SSL_CTX_clear_options(ssl_ctx, SSL_OP_LEGACY_SERVER_CONNECT);

_glootls::SSL_CTX_set_verify_depth(ssl_ctx, 1);

// To enforcing a higher security level, set it to 3.
//
// 2级
// 安全级别设置为 112 位安全。 因此,禁止使用短于 2048 位的 RSA、DSA 和 DH 密钥以及短于 224 位的 ECC 密钥。
// 除了 1 级排除之外,还禁止使用任何使用 RC4 的密码套件。 SSL 版本 3 也是不允许的。 压缩被禁用。
//
// Level 3
// 安全级别设置为 128 位安全。
// 因此,禁止使用小于 3072 位的 RSA、DSA 和 DHkey 以及小于 256 位的 ECC 密钥。
// 除了 2 级排除之外,禁止使用不提供前向保密的密码套件。 不允许使用低于 1.1 的 TLS 版本。 会话票证被禁用。
//
// TODO: should be 3, but it doesn't work yet :(
_glootls::SSL_CTX_set_security_level(ssl_ctx, 2);

GLOO_ENFORCE(
_glootls::SSL_CTX_load_verify_locations(ssl_ctx, ca_file, ca_path) == 1,
getSSLErrorMessage());
GLOO_ENFORCE(_glootls::SSL_CTX_use_certificate_chain_file(ssl_ctx, cert) == 1,
getSSLErrorMessage());
GLOO_ENFORCE(_glootls::SSL_CTX_use_PrivateKey_file(ssl_ctx, pkey,
SSL_FILETYPE_PEM) == 1,
getSSLErrorMessage());
GLOO_ENFORCE(_glootls::SSL_CTX_check_private_key(ssl_ctx) == 1,
getSSLErrorMessage());
// SSL_VERIFY_PEER
//
// 服务器模式:服务器向客户端发送客户端证书请求。
// 检查返回的证书(如果有)。 如果验证过程失败,TLS/SSL 握手会立即终止,并发出一条包含验证失败原因的警报消息。
// 该行为可以通过附加的 SSL_VERIFY_FAIL_IF_NO_PEER_CERT 和 SSL_VERIFY_CLIENT_ONCE 标志来控制。
//
// 客户端模式:验证服务器证书。
// 如果验证过程失败,TLS/SSL 握手会立即终止,并发出一条包含验证失败原因的警报消息。
// 如果没有发送服务器证书,因为使用了匿名密码,SSL_VERIFY_PEER 将被忽略。
_glootls::SSL_CTX_set_verify(ssl_ctx,
SSL_VERIFY_PEER |
SSL_VERIFY_FAIL_IF_NO_PEER_CERT |
SSL_VERIFY_CLIENT_ONCE,
nullptr);
return ssl_ctx;
}

正经tcp

同样需要创建通信对,缓冲区,以及处理通信等。

1
2
3
4
5
6
7
8
9
10
11
12
std::unique_ptr<transport::Pair>& Context::createPair(int rank) {
pairs_[rank] = std::unique_ptr<transport::Pair>(
new tcp::Pair(this, device_.get(), rank, getTimeout()));
return pairs_[rank];
}

std::unique_ptr<transport::UnboundBuffer> Context::createUnboundBuffer(
void* ptr,
size_t size) {
auto buf = new tcp::UnboundBuffer(shared_from_this(), ptr, size);
return std::unique_ptr<transport::UnboundBuffer>(buf);
}

以下是处理通信的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
void Context::recvFromAny(
UnboundBuffer* buf,
uint64_t slot,
size_t offset,
size_t nbytes,
std::vector<int> srcRanks) {
for (;;) {
// Find rank of pair we can attempt a recv from
auto rank = recvFromAnyFindRank(buf, slot, offset, nbytes, srcRanks);
if (rank == -1) {
return;
}
// Try recv from returned rank
auto ptr = pairs_[rank].get();
GLOO_ENFORCE(ptr != nullptr);
auto pair = dynamic_cast<Pair*>(ptr);
GLOO_ENFORCE(pair != nullptr);
if (pair->tryRecv(buf, slot, offset, nbytes)) {
return;
}
}
}

int Context::recvFromAnyFindRank(
UnboundBuffer* buf,
uint64_t slot,
size_t offset,
size_t nbytes,
const std::vector<int>& srcRanks) {
std::unique_lock<std::mutex> lock(mutex_);

// See if there is a remote pending send that can fulfill this recv.
auto it = findPendingOperations(slot);
if (it != pendingOperations_.end()) {
auto& pendingOperation = *it;

// Out of all remote pending sends, find the first one
// that exists in the set of eligible ranks.
for (const auto rank : pendingOperation.getSendList()) {
for (const auto srcRank : srcRanks) {
if (rank == srcRank) {
// 我们找到了一个可以满足这个recv的等级。
// 此函数的调用者将尝试进行recv,如果该远程挂起发送操作仍然存在,它将删除它。
//
return rank;
}
}
}
}

// No candidates; register buffer for recv
pendingRecv_[slot].emplace_back(
buf->getWeakNonOwningPtr(),
offset,
nbytes,
std::unordered_set<int>(srcRanks.begin(), srcRanks.end()));
return -1;
}

// Allowed to be called only by ContextMutator::findRecvFromAny,
// where the context lock is already held.
bool Context::findRecvFromAny(
uint64_t slot,
int rank,
WeakNonOwningPtr<UnboundBuffer>* buf,
size_t* offset,
size_t* nbytes) {
// See if there is a pending recv for this slot.
auto pit = pendingRecv_.find(slot);
if (pit != pendingRecv_.end()) {
auto& recvs = pit->second;

// Iterate over available buffers to find a match.
for (auto rit = recvs.begin(); rit != recvs.end(); rit++) {
const auto& ranks = std::get<3>(*rit);

// 如果此对等点的rank在此插槽的可接受rank集中,我们可以继续并将缓冲区返回到 recv 中。
if (ranks.count(rank) > 0) {
// Capture values to return.
*buf = std::get<0>(*rit);
*offset = std::get<1>(*rit);
*nbytes = std::get<2>(*rit);
// Cleanup.
recvs.erase(rit);
if (recvs.empty()) {
pendingRecv_.erase(pit);
}
return true;
}
}
}

return false;
}

loop

其中包含了epoll的使用方法,单独拿出来

创建一个epoll

1
2
3
4
5
Loop::Loop() {
fd_ = epoll_create(1);
GLOO_ENFORCE_NE(fd_, -1, "epoll_create: ", strerror(errno));
loop_.reset(new std::thread(&Loop::run, this));
}

epoll_ctl,用于操作epoll函数所生成的实例。

1
2
#include <sys / epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event * event);

该系统调用对文件描述符epfd引用的epoll实例执行控制操作。它要求操作op对目标文件描述符fd执行。op参数的有效值为:

  • EPOLL_CTL_ADD:在文件描述符epfd所引用的epoll实例上注册目标文件描述符fd,并将事件事件与内部文件链接到fd。
  • EPOLL_CTL_MOD:更改与目标文件描述符fd相关联的事件事件。
  • EPOLL_CTL_DEL:从epfd引用的epoll实例中删除(注销)目标文件描述符fd。该事件将被忽略,并且可以为NULL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Loop::registerDescriptor(int fd, int events, Handler* h) {
struct epoll_event ev;
ev.events = events;
ev.data.ptr = h;

auto rv = epoll_ctl(fd_, EPOLL_CTL_ADD, fd, &ev);
if (rv == -1 && errno == EEXIST) {
rv = epoll_ctl(fd_, EPOLL_CTL_MOD, fd, &ev);
}
GLOO_ENFORCE_NE(rv, -1, "epoll_ctl: ", strerror(errno));
}

void Loop::unregisterDescriptor(int fd, Handler* h) {
auto rv = epoll_ctl(fd_, EPOLL_CTL_DEL, fd, nullptr);
GLOO_ENFORCE_NE(rv, -1, "epoll_ctl: ", strerror(errno));

// Wait for loop to tick before returning, to make sure the handler
// for this fd is not called once this function returns.
if (std::this_thread::get_id() != loop_->get_id()) {
std::unique_lock<std::mutex> lock(m_);
cv_.wait(lock);
TSAN_ANNOTATE_HAPPENS_AFTER(h);
}
}

等待某个epoll完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void Loop::run() {
std::array<struct epoll_event, capacity_> events;
int nfds;

while (!done_) {
// Wakeup everyone waiting for a loop tick to finish.
cv_.notify_all();

// Wait for something to happen
nfds = epoll_wait(fd_, events.data(), events.size(), 10);
if (nfds == 0) {
continue;
}
if (nfds == -1 && errno == EINTR) {
continue;
}

GLOO_ENFORCE_NE(nfds, -1);

for (int i = 0; i < nfds; i++) {
Handler* h = reinterpret_cast<Handler*>(events[i].data.ptr);
h->handleEvents(events[i].events);
TSAN_ANNOTATE_HAPPENS_BEFORE(h);
}
}
}

mpi

mpi相关的通信操作,MPIScope应该是MPI的上下文什么的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::shared_ptr<MPIScope> getMPIScope() {
static std::once_flag once;

// Use weak pointer so that the initializer is destructed when the
// last context referring to it is destructed, not when statics
// are destructed on program termination.
static std::weak_ptr<MPIScope> wptr;
std::shared_ptr<MPIScope> sptr;

// Create MPIScope only once
std::call_once(once, [&]() {
sptr = std::make_shared<MPIScope>();
wptr = sptr;
});

// Create shared_ptr<MPIScope> from weak_ptr
sptr = wptr.lock();
GLOO_ENFORCE(sptr, "Cannot create MPI context after MPI_Finalize()");
return sptr;
}

返回MPI上下文(通信域)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::shared_ptr<Context> Context::createManaged() {
auto mpiScope = getMPIScope();
auto context = std::make_shared<Context>(MPI_COMM_WORLD);
context->mpiScope_ = std::move(mpiScope);
return context;
}

Context::Context(const MPI_Comm& comm)
: ::gloo::Context(MPICommRank(comm), MPICommSize(comm)) {
auto error = MPI_Comm_dup(comm, &comm_);
GLOO_ENFORCE(error == MPI_SUCCESS, "MPI_Comm_dup: ", error);
}

Context::~Context() {
MPI_Comm_free(&comm_);
}

为本进程和其他进程创建通信对pair和缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void Context::connectFullMesh(std::shared_ptr<transport::Device>& dev) {
std::vector<std::vector<char>> addresses(size);
unsigned long maxLength = 0;
int rv;

// Create pair to connect to every other node in the collective
auto transportContext = dev->createContext(rank, size);
transportContext->setTimeout(getTimeout());
for (int i = 0; i < size; i++) {
if (i == rank) {
continue;
}

auto& pair = transportContext->createPair(i);

// Store address for pair for this rank
auto address = pair->address().bytes();
maxLength = std::max(maxLength, address.size());
addresses[i] = std::move(address);
}

// Agree on maximum length so we can prepare buffers
rv = MPI_Allreduce(
MPI_IN_PLACE, &maxLength, 1, MPI_UNSIGNED_LONG, MPI_MAX, comm_);
if (rv != MPI_SUCCESS) {
GLOO_THROW_IO_EXCEPTION("MPI_Allreduce: ", rv);
}

// Prepare input and output
std::vector<char> in(size * maxLength);
std::vector<char> out(size * size * maxLength);
for (int i = 0; i < size; i++) {
if (i == rank) {
continue;
}

auto& address = addresses[i];
memcpy(in.data() + (i * maxLength), address.data(), address.size());
}

// Allgather to collect all addresses of all pairs
rv = MPI_Allgather(
in.data(), in.size(), MPI_BYTE, out.data(), in.size(), MPI_BYTE, comm_);
if (rv != MPI_SUCCESS) {
GLOO_THROW_IO_EXCEPTION("MPI_Allgather: ", rv);
}

// Connect every pair
for (int i = 0; i < size; i++) {
if (i == rank) {
continue;
}

auto offset = (rank + i * size) * maxLength;
std::vector<char> address(maxLength);
memcpy(address.data(), out.data() + offset, maxLength);
transportContext->getPair(i)->connect(address);
}

device_ = dev;
transportContext_ = std::move(transportContext);
}

通信

首先得获取本进程的左方和右方

1
2
3
4
5
6
7
8
9
10
11
12
13
// Helper for ring algorithms
std::unique_ptr<transport::Pair>& Algorithm::getLeftPair() {
auto rank = (context_->size + context_->rank - 1) % context_->size;
GLOO_ENFORCE(context_->getPair(rank), "pair missing (index ", rank, ")");
return context_->getPair(rank);
}

// Helper for ring algorithms
std::unique_ptr<transport::Pair>& Algorithm::getRightPair() {
auto rank = (context_->rank + 1) % context_->size;
GLOO_ENFORCE(context_->getPair(rank), "pair missing (index ", rank, ")");
return context_->getPair(rank);
}

gloo支持reduce的一些操作,对于一些reduce时自定义的方法,gloo也做了兼容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Type of reduction function.
// 如果reduce类型是内置类型之一,则算法实现可以使用加速版本(如果可用)。
// 例如,如果将 ReductionType 等于 SUM 的 ReductionFunction 传递给 CUDA 感知的 Allreduce,它知道它可以使用 NCCL 实现而不是指定的函数。
//
enum ReductionType {
SUM = 1,
PRODUCT = 2,
MAX = 3,
MIN = 4,

// Use larger number so we have plenty of room to add built-ins
CUSTOM = 1000,
};

template <typename T>
class ReductionFunction {
public:
using Function = void(T*, const T*, size_t n);

static const ReductionFunction<T>* sum;
static const ReductionFunction<T>* product;
static const ReductionFunction<T>* min;
static const ReductionFunction<T>* max;

ReductionFunction(ReductionType type, Function* fn)
: type_(type), fn_(fn) {}

ReductionType type() const {
return type_;
}

void call(T* x, const T* y, size_t n) const {
fn_(x, y, n);
}

protected:
ReductionType type_;
Function* fn_;
};

template <typename T>
const ReductionFunction<T>* ReductionFunction<T>::sum =
new ReductionFunction<T>(SUM, &::gloo::sum<T>);
template <typename T>
const ReductionFunction<T>* ReductionFunction<T>::product =
new ReductionFunction<T>(PRODUCT, &::gloo::product<T>);
template <typename T>
const ReductionFunction<T>* ReductionFunction<T>::min =
new ReductionFunction<T>(MIN, &::gloo::min<T>);
template <typename T>
const ReductionFunction<T>* ReductionFunction<T>::max =
new ReductionFunction<T>(MAX, &::gloo::max<T>);

// Local operation.
// If an algorithm uses multiple local pointers, local operations
// can be used for local reduction, broadcast, gathering, etc.
template <typename T>
class LocalOp {
public:
virtual ~LocalOp() noexcept(false) {}
virtual void runAsync() = 0;
virtual void wait() = 0;

// Synchronous run is equal to asynchronous run and wait.
inline void run() {
runAsync();
wait();
}
};

allgather

AllgatherRing 类似于 MPI_Allgather,所有进程都从所有其他进程接收缓冲区(inPtrs)。 调用者需要传递一个预先分配的接收缓冲区 (outPtr),其大小等于[ 上下文大小 x 发送缓冲区的总大小] (inPtrs),其中 rank = k 的进程的发送缓冲区将被写入 outPtr[k 输入缓冲区数量 count] 连续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template <typename T>
class AllgatherRing : public Algorithm {
public:
AllgatherRing(
const std::shared_ptr<Context>& context,
const std::vector<const T*>& inPtrs,
T* outPtr,
int count)
: Algorithm(context),
inPtrs_(inPtrs),
outPtr_(outPtr),
count_(count),
bytes_(count * sizeof(T)),
inputStride_(count_ * inPtrs_.size()),
leftPair_(this->getLeftPair()),
rightPair_(this->getRightPair()) {
auto slot = this->context_->nextSlot();

// std::unique_ptr<transport::Buffer>
sendDataBuf_ = rightPair_->createSendBuffer(
slot, outPtr_, inPtrs_.size() * context_->size * bytes_);
recvDataBuf_ = leftPair_->createRecvBuffer(
slot, outPtr_, inPtrs_.size() * context_->size * bytes_);

auto notificationSlot = this->context_->nextSlot();

// std::unique_ptr<transport::Buffer>
sendNotificationBuf_ =
leftPair_->createSendBuffer(notificationSlot, &dummy_, sizeof(dummy_));
recvNotificationBuf_ =
rightPair_->createRecvBuffer(notificationSlot, &dummy_, sizeof(dummy_));
}

真正运行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  void run() {
const int rank = this->contextRank_;
const int numRounds = this->contextSize_ - 1;

// Copy local buffers.
for (int i = 0; i < inPtrs_.size(); i++) {
memcpy(outPtr_ + rank * inputStride_ + i * count_, inPtrs_[i], bytes_);
}

// We send input buffers in order.
for (int i = 0; i < inPtrs_.size(); i++) {
// We start every iteration by sending local buffer.
int inRank = rank;

// 10个进程,就是9个round。1号进程,第一个round给0,第二个round给9,第三个round给8...
for (int round = 0; round < numRounds; round++) {
const int sendOffset = inRank * inputStride_ + i * count_;
sendDataBuf_->send(
sendOffset * sizeof(T), bytes_, sendOffset * sizeof(T));
recvDataBuf_->waitRecv();

// Nodes receive data from the left node in every round and forward it
// to the right node.
inRank = (numRounds - round + rank) % this->contextSize_;

// Send notification to node on the left that this node is ready for an
// inbox write.
sendNotificationBuf_->send();

// Wait for notification from node on the right.
recvNotificationBuf_->waitRecv();
}
}
}

private:
const std::vector<const T*> inPtrs_;
T* outPtr_;
const int count_;
const int bytes_;
const int inputStride_;

std::unique_ptr<transport::Pair>& leftPair_;
std::unique_ptr<transport::Pair>& rightPair_;

std::unique_ptr<transport::Buffer> sendDataBuf_;
std::unique_ptr<transport::Buffer> recvDataBuf_;

int dummy_;

std::unique_ptr<transport::Buffer> sendNotificationBuf_;
std::unique_ptr<transport::Buffer> recvNotificationBuf_;
};

一般的allgather

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void allgather(AllgatherOptions& opts) {
const auto& context = opts.context;
transport::UnboundBuffer* in = opts.in.get();
transport::UnboundBuffer* out = opts.out.get();
const auto slot = Slot::build(kAllgatherSlotPrefix, opts.tag);

const auto recvRank = (context->size + context->rank - 1) % context->size;
const auto sendRank = (context->size + context->rank + 1) % context->size;

const size_t inBytes = out->size / context->size;
const size_t outBytes = out->size;

// If the input buffer is specified, this is NOT an in place operation,
// and the output buffer needs to be primed with the input.
if (in != nullptr) {
memcpy(
static_cast<uint8_t*>(out->ptr) + context->rank * in->size,
static_cast<uint8_t*>(in->ptr),
in->size);
}

// Short circuit if there is only a single process.
if (context->size == 1) {
return;
}

// The chunk size may not be divisible by 2; use dynamic lookup.
std::array<size_t, 2> chunkSize;
chunkSize[0] = inBytes / 2;
chunkSize[1] = inBytes - chunkSize[0];
std::array<size_t, 2> chunkOffset;
chunkOffset[0] = 0;
chunkOffset[1] = chunkSize[0];

// 10个进程,1号进程
// send to 2,recv from 0
// send seg = 11 11 10 10 9 9 8 8 ...
// recv seg = 10 10 9 9 8 8 7 7 ...
// i & 0x1 = 0 1 0 1 0 1 0 1 ...

for (auto i = 0; i < (context->size - 1) * 2; i++) {
const size_t sendSegment = context->size + context->rank - (i / 2);
const size_t recvSegment = sendSegment - 1;
size_t sendOffset =
((sendSegment * inBytes) + chunkOffset[i & 0x1]) % outBytes;
size_t recvOffset =
((recvSegment * inBytes) + chunkOffset[i & 0x1]) % outBytes;
size_t size = chunkSize[i & 0x1];
if (i < 2) {
out->send(sendRank, slot, sendOffset, size);
out->recv(recvRank, slot, recvOffset, size);
continue;
}

// Wait for pending operations to complete to synchronize with the
// previous iteration. Because we kick off two operations before
// getting here we always wait for the next-to-last operation.
out->waitSend(opts.timeout);
out->waitRecv(opts.timeout);
out->send(sendRank, slot, sendOffset, size);
out->recv(recvRank, slot, recvOffset, size);
}

// Wait for completes
for (auto i = 0; i < 2; i++) {
out->waitSend(opts.timeout);
out->waitRecv(opts.timeout);
}
}

allgatherv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void allgatherv(AllgathervOptions& opts) {
const auto& context = opts.context;
transport::UnboundBuffer* in = opts.in.get();
transport::UnboundBuffer* out = opts.out.get();
const auto slot = Slot::build(kAllgatherSlotPrefix, opts.tag);

const auto recvRank = (context->size + context->rank - 1) % context->size;
const auto sendRank = (context->size + context->rank + 1) % context->size;

// 计算每个进程对应的长度和偏移
std::vector<size_t> byteCounts;
std::vector<size_t> byteOffsets;
byteCounts.reserve(context->size);
byteOffsets.reserve(context->size);
size_t offset = 0;
for (const auto& elements : opts.elements) {
const auto bytes = elements * opts.elementSize;
byteCounts.push_back(bytes);
byteOffsets.push_back(offset);
offset += bytes;
}

// 如果指定了输入缓冲区,则需要准备输出缓冲区。
if (in != nullptr) {
GLOO_ENFORCE_EQ(byteCounts[context->rank], in->size);
if (byteCounts[context->rank] > 0) {
memcpy(
static_cast<uint8_t*>(out->ptr) + byteOffsets[context->rank],
static_cast<uint8_t*>(in->ptr),
in->size);
}
}

// Short circuit if there is only a single process.
if (context->size == 1) {
return;
}

const auto baseIndex = context->size + context->rank;
for (auto i = 0; i < context->size - 1; i++) {
const size_t sendIndex = (baseIndex - i) % context->size;
const size_t recvIndex = (baseIndex - i - 1) % context->size;

if (i == 0) {
out->send(sendRank, slot, byteOffsets[sendIndex], byteCounts[sendIndex]);
out->recv(recvRank, slot, byteOffsets[recvIndex], byteCounts[recvIndex]);
continue;
}

// Wait for previous operations to complete before kicking off new ones.
out->waitSend(opts.timeout);
out->waitRecv(opts.timeout);
out->send(sendRank, slot, byteOffsets[sendIndex], byteCounts[sendIndex]);
out->recv(recvRank, slot, byteOffsets[recvIndex], byteCounts[recvIndex]);
}

// Wait for final operations to complete.
out->waitSend(opts.timeout);
out->waitRecv(opts.timeout);
}

allreduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
using BufferVector = std::vector<std::unique_ptr<transport::UnboundBuffer>>;
using ReductionFunction = AllreduceOptions::Func;
using ReduceRangeFunction = std::function<void(size_t, size_t)>;
using BroadcastRangeFunction = std::function<void(size_t, size_t)>;

// Forward declaration of ring algorithm implementation.
void ring(
const detail::AllreduceOptionsImpl& opts,
ReduceRangeFunction reduceInputs,
BroadcastRangeFunction broadcastOutputs);

// Forward declaration of bcube algorithm implementation.
void bcube(
const detail::AllreduceOptionsImpl& opts,
ReduceRangeFunction reduceInputs,
BroadcastRangeFunction broadcastOutputs);

// ReductionFunction type describes the function to use for element wise reduction.
//
// Its arguments are:
// 1. non-const output pointer
// 2. const input pointer 1 (may be equal to 1)
// 3. const input pointer 2 (may be equal to 1)
// 4. number of elements to reduce.
//
// 请注意,此函数不是严格类型的,并且采用 void 指针。
// 这样做是为了避免需要模板化选项类和模板化算法实现。
// 我们发现这对编译时间和代码大小的增加几乎没有任何价值。s

// 返回计算输入的局部reduce并将其存储在这些缓冲区中给定范围的输出中的函数。
// 这是在向邻居发送区域或reduce从邻居接收的区域之前完成的。
ReduceRangeFunction genLocalReduceFunction(
const BufferVector& in, // UnboundBuffer的unique_ptr的vector
const BufferVector& out,
size_t elementSize,
ReductionFunction fn) {
// 根据传进来的buffer长度,执行reduce函数
if (in.size() > 0) {
if (in.size() == 1) {
return [&in, &out](size_t offset, size_t length) {
memcpy(
static_cast<uint8_t*>(out[0]->ptr) + offset,
static_cast<const uint8_t*>(in[0]->ptr) + offset,
length);
};
} else {
return [&in, &out, elementSize, fn](size_t offset, size_t length) {
fn(static_cast<uint8_t*>(out[0]->ptr) + offset,
static_cast<const uint8_t*>(in[0]->ptr) + offset,
static_cast<const uint8_t*>(in[1]->ptr) + offset,
length / elementSize);
for (size_t i = 2; i < in.size(); i++) {
fn(static_cast<uint8_t*>(out[0]->ptr) + offset,
static_cast<const uint8_t*>(out[0]->ptr) + offset,
static_cast<const uint8_t*>(in[i]->ptr) + offset,
length / elementSize);
}
};
}
} else {
return [&out, elementSize, fn](size_t offset, size_t length) {
for (size_t i = 1; i < out.size(); i++) {
fn(static_cast<uint8_t*>(out[0]->ptr) + offset,
static_cast<const uint8_t*>(out[0]->ptr) + offset,
static_cast<const uint8_t*>(out[i]->ptr) + offset,
length / elementSize);
}
};
}
}

// 返回对缓冲区中给定范围的输出执行本地广播的函数。 这是在接收到每个全局reduce的块之后执行的。
BroadcastRangeFunction genLocalBroadcastFunction(const BufferVector& out) {
return [&out](size_t offset, size_t length) {
for (size_t i = 1; i < out.size(); i++) {
memcpy(
static_cast<uint8_t*>(out[i]->ptr) + offset,
static_cast<const uint8_t*>(out[0]->ptr) + offset,
length);
}
};
}

void allreduce(const detail::AllreduceOptionsImpl& opts) {
if (opts.elements == 0) {
return;
}

const auto& context = opts.context;
const std::vector<std::unique_ptr<transport::UnboundBuffer>>& in = opts.in;
const std::vector<std::unique_ptr<transport::UnboundBuffer>>& out = opts.out;
const auto slot = Slot::build(kAllreduceSlotPrefix, opts.tag);

// 初始化本地归约和广播功能。
// 请注意,如果仅指定单个输出并将其用作输入和输出,则这些是无操作的。
const auto reduceInputs =
genLocalReduceFunction(in, out, opts.elementSize, opts.reduce);
const auto broadcastOutputs = genLocalBroadcastFunction(out);

// Simple circuit if there is only a single process.
if (context->size == 1) {
reduceInputs(0, totalBytes);
broadcastOutputs(0, totalBytes);
return;
}

switch (opts.algorithm) {
case detail::AllreduceOptionsImpl::UNSPECIFIED:
case detail::AllreduceOptionsImpl::RING:
ring(opts, reduceInputs, broadcastOutputs);
break;
case detail::AllreduceOptionsImpl::BCUBE:
bcube(opts, reduceInputs, broadcastOutputs);
break;
default:
GLOO_ENFORCE(false, "Algorithm not handled.");
}
}

allreduce的ring方法

给定的输入被分成与进程数相等的块数。 算法完成后,每个进程按顺序托管一个reduction输出块(rank 0 具有块 0,rank 1 具有块 1,等等)。由于输入可能不能被进程数整除,因此最终的块有部分输出或可能为空。

当一个块沿着环传递并且包含连续更多rank的reduction时,我们必须在为该块执行 I/O 和计算接收到的块和本地块之间的reduction之间交替。为了避免这种交替模式,我们将一个块分成多个段(> = 2),并确保我们有一个段在运行,同时计算另一个段的reduction。

段大小有一个上限,以最大限度地减少内存使用并避免不良的缓存行为。 这意味着在处理非常大的输入时,每个块可能有很多段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
void ring(
const detail::AllreduceOptionsImpl& opts,
ReduceRangeFunction reduceInputs,
BroadcastRangeFunction broadcastOutputs) {
const auto& context = opts.context;
const std::vector<std::unique_ptr<transport::UnboundBuffer>>& out = opts.out;
const auto slot = Slot::build(kAllreduceSlotPrefix, opts.tag);
const size_t totalBytes = opts.elements * opts.elementSize;

// Note: context->size > 1
const auto recvRank = (context->size + context->rank + 1) % context->size;
const auto sendRank = (context->size + context->rank - 1) % context->size;

// 确保最大段大小是元素大小的倍数。
// 否则,在向上舍入到元素大小的最接近倍数后,段大小可能会超过最大段大小。
// 例如,如果maxSegmentSize = 10,而elementSize = 4,则向上取整后:segmentSize = 12;
const size_t maxSegmentBytes = opts.elementSize *
std::max((size_t)1, opts.maxSegmentSize / opts.elementSize);

// Compute how many segments make up the input buffer.
//
// 向上舍入到上下文大小的最接近的倍数,以便每个进程有相同数量的段,并且跨进程的执行是对称的。
// 最小值是上下文大小的两倍,因为下面的算法将发送/接收一个段与计算另一个段的reduction。
const size_t numSegments = roundUp(
std::max(
(totalBytes + (maxSegmentBytes - 1)) / maxSegmentBytes,
(size_t)context->size * 2),
(size_t)context->size);

const size_t numSegmentsPerRank = numSegments / context->size;
const size_t segmentBytes =
roundUp((totalBytes + numSegments - 1) / numSegments, opts.elementSize);

// Allocate scratch space to hold two chunks
std::unique_ptr<uint8_t[]> tmpAllocation(new uint8_t[segmentBytes * 2]);
std::unique_ptr<transport::UnboundBuffer> tmpBuffer =
context->createUnboundBuffer(tmpAllocation.get(), segmentBytes * 2);
transport::UnboundBuffer* tmp = tmpBuffer.get();

// 使用动态查找临时缓冲区中的块偏移量。
// 在进行两个操作时,我们需要两个偏移量。 可以使用循环计数器对它们进行索引。
std::array<size_t, 2> segmentOffset;
segmentOffset[0] = 0;
segmentOffset[1] = segmentBytes;

// 计算在reduce/scatter为给定迭代发送和接收的段的偏移量和长度。
auto computeReduceScatterOffsets = [&](size_t i) {
struct {
size_t sendOffset;
size_t recvOffset;
ssize_t sendLength;
ssize_t recvLength;
} result;

// 计算要发送的段索引(到 rank-1)和要接收的段索引(从 rank+1)。
// 乘以块中的字节数以获得偏移量。
// 允许偏移量超出范围(>=totalBytes),计算相关长度时会考虑到这一点。
result.sendOffset =
((((context->rank + 1) * numSegmentsPerRank) + i) * segmentBytes) %
(numSegments * segmentBytes);
result.recvOffset =
((((context->rank + 2) * numSegmentsPerRank) + i) * segmentBytes) %
(numSegments * segmentBytes);

// If the segment is entirely in range, the following statement is
// equal to segmentBytes. If it isn't, it will be less, or even
// negative. This is why the ssize_t typecasts are needed.
result.sendLength = std::min(
(ssize_t)segmentBytes,
(ssize_t)totalBytes - (ssize_t)result.sendOffset);
result.recvLength = std::min(
(ssize_t)segmentBytes,
(ssize_t)totalBytes - (ssize_t)result.recvOffset);

return result;
};

// Ring reduce/scatter.
//
// 迭代次数计算如下:
// - 使用 `numSegments` 作为段的总数,
// - 减去 `numSegmentsPerRank`,因为最终段包含部分结果,在此阶段不得转发。
// - 添加 2,因为我们通过管道发送和接收操作(我们在迭代 0 和 1 上发出发送/接收操作并等待它们在迭代 2 和 3 上完成)。
//
for (auto i = 0; i < (numSegments - numSegmentsPerRank + 2); i++) {
if (i >= 2) {
// 计算两次迭代前的发送和接收偏移量和长度。
// 需要这样我们知道何时等待操作以及何时忽略(当偏移量超出范围时),并知道在哪里减少临时缓冲区的内容。
auto prev = computeReduceScatterOffsets(i - 2);
if (prev.recvLength > 0) {
// Prepare out[0]->ptr to hold the local reduction
reduceInputs(prev.recvOffset, prev.recvLength);
// Wait for segment from neighbor.
tmp->waitRecv(opts.timeout);
// 对收到的段进行reduce
opts.reduce(
static_cast<uint8_t*>(out[0]->ptr) + prev.recvOffset,
static_cast<const uint8_t*>(out[0]->ptr) + prev.recvOffset,
static_cast<const uint8_t*>(tmp->ptr) + segmentOffset[i & 0x1],
prev.recvLength / opts.elementSize);
}
if (prev.sendLength > 0) {
out[0]->waitSend(opts.timeout);
}
}

// 在最后两次迭代之外的所有迭代中发出新的发送和接收操作。
// 那时我们已经发送了我们需要的所有数据,只需要等待最终的段被reduce到输出中。
if (i < (numSegments - numSegmentsPerRank)) {
// Compute send and receive offsets and lengths for this iteration.
auto cur = computeReduceScatterOffsets(i);
if (cur.recvLength > 0) {
tmp->recv(recvRank, slot, segmentOffset[i & 0x1], cur.recvLength);
}
if (cur.sendLength > 0) {
// Prepare out[0]->ptr to hold the local reduction for this segment
if (i < numSegmentsPerRank) {
reduceInputs(cur.sendOffset, cur.sendLength);
}
out[0]->send(sendRank, slot, cur.sendOffset, cur.sendLength);
}
}
}

// Function computes the offsets and lengths of the segments to be
// sent and received for a given iteration during allgather.
auto computeAllgatherOffsets = [&](size_t i) {
struct {
size_t sendOffset;
size_t recvOffset;
ssize_t sendLength;
ssize_t recvLength;
} result;

result.sendOffset =
((((context->rank) * numSegmentsPerRank) + i) * segmentBytes) %
(numSegments * segmentBytes);
result.recvOffset =
((((context->rank + 1) * numSegmentsPerRank) + i) * segmentBytes) %
(numSegments * segmentBytes);

// If the segment is entirely in range, the following statement is
// equal to segmentBytes. If it isn't, it will be less, or even
// negative. This is why the ssize_t typecasts are needed.
result.sendLength = std::min(
(ssize_t)segmentBytes,
(ssize_t)totalBytes - (ssize_t)result.sendOffset);
result.recvLength = std::min(
(ssize_t)segmentBytes,
(ssize_t)totalBytes - (ssize_t)result.recvOffset);

return result;
};

// Ring allgather.
//
// 注意:totalBytes <= (numSegments * segmentBytes),
// 这与在进程间贡献相同的通用 allgather 算法不兼容。
//
for (auto i = 0; i < (numSegments - numSegmentsPerRank + 2); i++) {
if (i >= 2) {
auto prev = computeAllgatherOffsets(i - 2);
if (prev.recvLength > 0) {
out[0]->waitRecv(opts.timeout);
// Broadcast received segments to output buffers.
broadcastOutputs(prev.recvOffset, prev.recvLength);
}
if (prev.sendLength > 0) {
out[0]->waitSend(opts.timeout);
}
}

// 在最后两次迭代之外的所有迭代中发出新的发送和接收操作。
// 那时我们已经发送了我们需要的所有数据,只需要等待最终的段被发送到输出。
if (i < (numSegments - numSegmentsPerRank)) {
auto cur = computeAllgatherOffsets(i);
if (cur.recvLength > 0) {
out[0]->recv(recvRank, slot, cur.recvOffset, cur.recvLength);
}
if (cur.sendLength > 0) {
out[0]->send(sendRank, slot, cur.sendOffset, cur.sendLength);
// Broadcast first segments to outputs buffers.
if (i < numSegmentsPerRank) {
broadcastOutputs(cur.sendOffset, cur.sendLength);
}
}
}
}
}

// 对于给定的上下文大小和所需的组大小,计算每步的实际组大小。
// 请注意,对于所有步骤,每一步的组大小为 n,仅当 n^(#steps) == 大小时。
// 否则,最终组大小为 != n。
std::vector<size_t> computeGroupSizePerStep(size_t size, const size_t n) {
std::vector<size_t> result;
GLOO_ENFORCE_GT(n, 1);
while (size % n == 0) {
result.push_back(n);
size /= n;
}
if (size > 1) {
result.push_back(size);
}
return result;
}

bcube 算法

bcube 算法实现了一种类似超立方体的reduce策略。约束是进程的数量可以分解。如果分解中的最小分量为 2,并且进程数等于 2 的幂,则该算法与递归减半/加倍相同。

分解中的元素数量决定了算法的步数。分解的每个元素决定了每个进程在算法的特定步骤中与之通信的进程数。如果进程数不可分解,则该算法与直接reduce-scatter 后allgather 相同。

例如,如果#processes == 8,并且我们将其分解为 4 * 2,则算法分 2 步运行。在第一步中,2 组 4 个进程之间交换数据,以使所有进程具有部分结果的 1/4。第二步,4组2个进程交换它们的部分结果,使得所有进程都有1/8的结果。然后,反向执行相同的分解以执行 allgather。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
void bcube(
const detail::AllreduceOptionsImpl& opts,
ReduceRangeFunction reduceInputs,
BroadcastRangeFunction broadcastOutputs) {
const auto& context = opts.context;
const auto slot = Slot::build(kAllreduceSlotPrefix, opts.tag);
const auto elementSize = opts.elementSize;
auto& out = opts.out[0];

constexpr auto n = 2;

// 算出这个算法的步数。
const auto groupSizePerStep = computeGroupSizePerStep(context->size, n);

struct group {
// Distance between peers in this group.
size_t peerDistance;

// Segment that this group is responsible for reducing.
size_t bufferOffset;
size_t bufferLength;

// The process ranks that are a member of this group.
std::vector<size_t> ranks;

// Upper bound of the length of the chunk that each process has the
// reduced values for by the end of the reduction for this group.
size_t chunkLength;

// Chunk within the segment that this process is responsible for reducing.
size_t myChunkOffset;
size_t myChunkLength;
};

// 在每个算法步骤计算组的详细信息。
// 我们将它保存在一个向量中,因为我们在reduce/scatter阶段以正序迭代它,在全聚集阶段以反向顺序迭代它。
std::vector<struct group> groups;
{
struct group group;
group.peerDistance = 1;
group.bufferOffset = 0;
group.bufferLength = opts.elements;
for (const size_t groupSize : groupSizePerStep) {
const size_t groupRank = (context->rank / group.peerDistance) % groupSize;
const size_t baseRank = context->rank - (groupRank * group.peerDistance);
group.ranks.reserve(groupSize);
for (size_t i = 0; i < groupSize; i++) {
group.ranks.push_back(baseRank + i * group.peerDistance);
}
// 每隔groupSize个进程是一组,也就是说一个组内的rank都隔着groupSize

// Compute the length of the chunk we're exchanging at this step.
group.chunkLength = ((group.bufferLength + (groupSize - 1)) / groupSize);

// 此过程正在计算当前段中位于 <rank>/<size> 的块的减少量。
//
group.myChunkOffset =
group.bufferOffset + (groupRank * group.chunkLength);
group.myChunkLength = std::min(
size_t(group.chunkLength),
size_t(std::max(
int64_t(0),
int64_t(group.bufferLength) -
int64_t(groupRank * group.chunkLength))));

// Store a const copy of this group in the vector.
groups.push_back(group);

// 使用更新的对等距离和段偏移和长度进行初始化。
struct group nextGroup;
nextGroup.peerDistance = group.peerDistance * groupSize;
nextGroup.bufferOffset = group.myChunkOffset;
nextGroup.bufferLength = group.myChunkLength;
std::swap(group, nextGroup);
}
}

// 块长度向上取整,因此我们需要的最大暂存空间可能大于输出缓冲区的大小。 计算最大值
size_t bufferLength = opts.elements;
for (const auto& group : groups) {
bufferLength =
std::max(bufferLength, group.ranks.size() * group.chunkLength);
}

// 分配暂存空间以从对等方接收数据。
const size_t bufferSize = bufferLength * elementSize;
std::unique_ptr<uint8_t[]> buffer(new uint8_t[bufferSize]);
std::unique_ptr<transport::UnboundBuffer> tmp =
context->createUnboundBuffer(buffer.get(), bufferSize);

// Reduce/scatter.
for (size_t step = 0; step < groups.size(); step++) {
const auto& group = groups[step];

// 从对等点发出块的接收操作。
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto src = group.ranks[i];
if (src == context->rank) {
continue;
}
tmp->recv(
src,
slot,
i * group.chunkLength * elementSize,
group.myChunkLength * elementSize);
}

// 向对等方发出本地块的发送操作。
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto dst = group.ranks[i];
if (dst == context->rank) {
continue;
}
const size_t currentChunkOffset =
group.bufferOffset + i * group.chunkLength;
const size_t currentChunkLength = std::min(
size_t(group.chunkLength),
size_t(std::max(
int64_t(0),
int64_t(group.bufferLength) - int64_t(i * group.chunkLength))));
// 仅在算法的第一步中计算局部reduce。
// 在随后的步骤中,我们已经得到了部分reduce的结果。
if (step == 0) {
reduceInputs(
currentChunkOffset * elementSize, currentChunkLength * elementSize);
}
out->send(
dst,
slot,
currentChunkOffset * elementSize,
currentChunkLength * elementSize);
}

// Wait for send and receive operations to complete.
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto peer = group.ranks[i];
if (peer == context->rank) {
continue;
}
tmp->waitRecv();
out->waitSend();
}

// 第一步,准备这个进程负责的chunk
// 使用其输入的简化版本(如果指定了多个)。
if (step == 0) {
reduceInputs(
group.myChunkOffset * elementSize, group.myChunkLength * elementSize);
}

// Reduce chunks from peers.
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto src = group.ranks[i];
if (src == context->rank) {
continue;
}
opts.reduce(
static_cast<uint8_t*>(out->ptr) + (group.myChunkOffset * elementSize),
static_cast<const uint8_t*>(out->ptr) +
(group.myChunkOffset * elementSize),
static_cast<const uint8_t*>(tmp->ptr) +
(i * group.chunkLength * elementSize),
group.myChunkLength);
}
}

// 有一个块包含最终结果,并且该块已经可以在本地广播到 out[1..N](如果适用)。
// 这样做意味着我们只需要在本地广播到 out[1..N] 所有块,因为我们在 allgather 阶段从对等方接收到它们。
{
const auto& group = groups.back();
broadcastOutputs(
group.myChunkOffset * elementSize, group.myChunkLength * elementSize);
}

// Allgather.
for (auto it = groups.rbegin(); it != groups.rend(); it++) {
const auto& group = *it;

// Issue receive operations for reduced chunks from peers.
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto src = group.ranks[i];
if (src == context->rank) {
continue;
}
const size_t currentChunkOffset =
group.bufferOffset + i * group.chunkLength;
const size_t currentChunkLength = std::min(
size_t(group.chunkLength),
size_t(std::max(
int64_t(0),
int64_t(group.bufferLength) - int64_t(i * group.chunkLength))));
out->recv(
src,
slot,
currentChunkOffset * elementSize,
currentChunkLength * elementSize);
}

// Issue send operations for reduced chunk to peers.
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto dst = group.ranks[i];
if (dst == context->rank) {
continue;
}
out->send(
dst,
slot,
group.myChunkOffset * elementSize,
group.myChunkLength * elementSize);
}

// Wait for operations to complete.
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto peer = group.ranks[i];
if (peer == context->rank) {
continue;
}
out->waitRecv();
out->waitSend();
}

// Broadcast result to multiple output buffers, if applicable.
for (size_t i = 0; i < group.ranks.size(); i++) {
const auto peer = group.ranks[i];
if (peer == context->rank) {
continue;
}
const size_t currentChunkOffset =
group.bufferOffset + i * group.chunkLength;
const size_t currentChunkLength = std::min(
size_t(group.chunkLength),
size_t(std::max(
int64_t(0),
int64_t(group.bufferLength) - int64_t(i * group.chunkLength))));
broadcastOutputs(
currentChunkOffset * elementSize, currentChunkLength * elementSize);
}
}
}

alltoallv

同alltoall一样,只不过alltoallv的实现多了offset,没有什么高深的算法,只是在send-recv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void alltoallv(AlltoallvOptions& opts) {
const auto& context = opts.context;
transport::UnboundBuffer* in = opts.in.get();
transport::UnboundBuffer* out = opts.out.get();
std::vector<size_t>& inOffsetPerRank = opts.inOffsetPerRank;
std::vector<size_t>& inLengthPerRank = opts.inLengthPerRank;
std::vector<size_t>& outOffsetPerRank = opts.outOffsetPerRank;
std::vector<size_t>& outLengthPerRank = opts.outLengthPerRank;
const auto slot = Slot::build(kAlltoallSlotPrefix, opts.tag);

int myRank = context->rank;
int worldSize = context->size;

// Local copy.
GLOO_ENFORCE(inLengthPerRank[myRank] == outLengthPerRank[myRank]);
size_t myInOffset = inOffsetPerRank[myRank];
size_t myOutOffset = outOffsetPerRank[myRank];
size_t myChunkSize = inLengthPerRank[myRank];
memcpy(
static_cast<char*>(out->ptr) + myOutOffset,
static_cast<char*>(in->ptr) + myInOffset,
myChunkSize);

// Remote copy.
for (int i = 1; i < worldSize; i++) {
int sendRank = (myRank + i) % worldSize;
int recvRank = (myRank + worldSize - i) % worldSize;
in->send(
sendRank, slot, inOffsetPerRank[sendRank], inLengthPerRank[sendRank]);
out->recv(
recvRank, slot, outOffsetPerRank[recvRank], outLengthPerRank[recvRank]);
}

for (int i = 1; i < worldSize; i++) {
in->waitSend(opts.timeout);
out->waitRecv(opts.timeout);
}
}

barrier

如果是共有16进程的话,

0号进程会与15 1,14 2,12 4,8 8进程recv send

1号进程会与0 2,15 3,13 5,9 9进程recv send

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void barrier(BarrierOptions& opts) {
const auto& context = opts.context;
auto& buffer = opts.buffer;
const auto slot = Slot::build(kBarrierSlotPrefix, opts.tag);

// Below implements a dissemination barrier, described in "Two algorithms
// for barrier synchronization (1988)" by Hensgen, Finkel and Manber.
// PDF: https://www.inf.ed.ac.uk/teaching/courses/ppls/BarrierPaper.pdf
// DOI: 10.1007/BF01379320

// Instead of iterating over i up to log2(context->size), we immediately
// compute 2^i and compare with context->size.
for (size_t d = 1; d < context->size; d <<= 1) {
buffer->recv((context->size + context->rank - d) % context->size, slot);
buffer->send((context->size + context->rank + d) % context->size, slot);
buffer->waitRecv(opts.timeout);
buffer->waitSend(opts.timeout);
}
}

broadcast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void broadcast(BroadcastOptions& opts) {
const auto& context = opts.context;
transport::UnboundBuffer* in = opts.in.get();
transport::UnboundBuffer* out = opts.out.get();
const auto slot = Slot::build(kBroadcastSlotPrefix, opts.tag);

if (context->rank == opts.root) {
in = out;
}

// 将rank映射到根进程rank为 0 的新rank。
const size_t vsize = context->size;
const size_t vrank = (context->rank + vsize - opts.root) % vsize;
const size_t dim = log2ceil(vsize);

// 跟踪未决发送操作的数量。
// 发送操作可以异步完成,因为迭代之间存在依赖关系。
// 这与必须在任何发送操作排队之前完成的 recv 操作不同。
size_t numSends = 0;

// 创建全为 1 的掩码,我们从 LSB 开始逐步将位设置为 0。
// 当应用于虚拟rank的掩码等于 0 时,我们知道该进程必须参与。
// 这导致从虚拟rank 0 和 1 开始的指数级参与.
size_t mask = (1 << dim) - 1;

for (size_t i = 0; i < dim; i++) {
// Clear bit `i`. 在第一次迭代中,虚拟rank 0 和 1 参与。
// 在第二次迭代中,0、1、2 和 3 参与,依此类推。
mask ^= (1 << i);
if ((vrank & mask) != 0) {
continue;
}

// The virtual rank of the peer in this iteration has opposite bit `i`.
auto vpeer = vrank ^ (1 << i);
if (vpeer >= vsize) {
continue;
}

// Map virtual rank of peer to actual rank of peer.
auto peer = (vpeer + opts.root) % vsize;
if ((vrank & (1 << i)) == 0) {
in->send(peer, slot);
numSends++;
} else {
out->recv(peer, slot);
out->waitRecv(opts.timeout);
}
}

// Copy local input to output if applicable.
if (context->rank == opts.root && in != out) {
memcpy(out->ptr, in->ptr, out->size);
}

// Wait on pending sends.
for (auto i = 0; i < numSends; i++) {
in->waitSend(opts.timeout);
}
}

avx优化

一些reduce函数使用了avx。_mm256_cvtph_ps将八个半精度(16 位)浮点值转换为单精度浮点值。_mm256_cvtps_ph将八个单精度浮点值转换为半精度(16 位)浮点值。_mm_storeu_si128将计算结果等SSE暂存器的数据保存到内存中。_mm256_mul_ps对第一个源向量 m1 中的八个压缩单精度浮点元素(float32 元素)与第二个源向量 m2 中的八个 float32 元素执行 SIMD 乘法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

//假设 x 和 y 要么都对齐到 32 字节,要么未对齐相同的偏移量,就像在对齐缓冲区内的偏移量处减少时会发生的那样
template <>
void sum<float16>(void* c_, const void* a_, const void* b_, size_t n) {
float16* c = static_cast<float16*>(c_);
const float16* a = static_cast<const float16*>(a_);
const float16* b = static_cast<const float16*>(b_);
size_t i;
for (i = 0; i < (n / 8) * 8; i += 8) {
__m256 va32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&a[i])));
__m256 vb32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&b[i])));
__m128i vc16 = _mm256_cvtps_ph(_mm256_add_ps(va32, vb32), 0);
_mm_storeu_si128((__m128i*)(&c[i]), vc16);
}
// Leftovers
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}

template <>
void product<float16>(void* c_, const void* a_, const void* b_, size_t n) {
float16* c = static_cast<float16*>(c_);
const float16* a = static_cast<const float16*>(a_);
const float16* b = static_cast<const float16*>(b_);
size_t i;
for (i = 0; i < (n / 8) * 8; i += 8) {
__m256 va32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&a[i])));
__m256 vb32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&b[i])));
__m128i vc16 = _mm256_cvtps_ph(_mm256_mul_ps(va32, vb32), 0);
_mm_storeu_si128((__m128i*)(&c[i]), vc16);
}
// Leftovers
for (; i < n; i++) {
c[i] = a[i] * b[i];
}
}

template <>
void max<float16>(void* c_, const void* a_, const void* b_, size_t n) {
float16* c = static_cast<float16*>(c_);
const float16* a = static_cast<const float16*>(a_);
const float16* b = static_cast<const float16*>(b_);
size_t i;
for (i = 0; i < (n / 8) * 8; i += 8) {
__m256 va32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&a[i])));
__m256 vb32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&b[i])));
__m128i vc16 = _mm256_cvtps_ph(_mm256_max_ps(va32, vb32), 0);
_mm_storeu_si128((__m128i*)(&c[i]), vc16);
}
// Leftovers
for (; i < n; i++) {
c[i] = std::max(a[i], b[i]);
}
}

template <>
void min<float16>(void* c_, const void* a_, const void* b_, size_t n) {
float16* c = static_cast<float16*>(c_);
const float16* a = static_cast<const float16*>(a_);
const float16* b = static_cast<const float16*>(b_);
size_t i;
for (i = 0; i < (n / 8) * 8; i += 8) {
__m256 va32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&a[i])));
__m256 vb32 = _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)(&b[i])));
__m128i vc16 = _mm256_cvtps_ph(_mm256_min_ps(va32, vb32), 0);
_mm_storeu_si128((__m128i*)(&c[i]), vc16);
}
// Leftovers
for (; i < n; i++) {
c[i] = std::min(a[i], b[i]);
}
}

reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
void reduce(ReduceOptions& opts) {
if (opts.elements == 0) {
return;
}
const auto& context = opts.context;
transport::UnboundBuffer* in = opts.in.get();
transport::UnboundBuffer* out = opts.out.get();
const auto slot = Slot::build(kReduceSlotPrefix, opts.tag);

const auto recvRank = (context->size + context->rank + 1) % context->size;
const auto sendRank = (context->size + context->rank - 1) % context->size;

// If input buffer is not specified, the output is also the input
if (in == nullptr) {
in = out;
}

// 如果只有一个进程,则短路。
if (context->size == 1) {
if (in != out) {
memcpy(out->ptr, in->ptr, opts.elements * opts.elementSize);
}
return;
}

// The ring algorithm works as follows.
//
// 给定的输入被分成与进程数相等的块数。
// 算法完成后,每个进程按顺序托管一个reduce输出块(rank 0 具有块 0,rank 1 具有块 1,等等)。
// 由于输入可能不能被进程数整除,因此最终的块可能有部分输出或可能为空。
//
// 当一个块沿着环传递并包含连续更多rank的reduce时,我们必须在为该块执行 I/O 和计算接收到的块和本地块之间的减少之间交替。
// 为了避免这种交替模式,我们将一个块分成多个段(> = 2),并确保我们有一个段在运行,同时计算另一个段的reduce。
// 段大小有一个上限,以最大限度地减少内存使用并避免不良的缓存行为。这意味着在处理非常大的输入时,每个块可能有很多段。
//
// 这里的命名法反映在下面的变量命名中(每个rank一个块,每个块多个段)。
//
const size_t totalBytes = opts.elements * opts.elementSize;

// 确保最大段大小是元素大小的倍数。 否则,在向上舍入到元素大小的最接近倍数后,段大小可能会超过最大段大小。 例如,如果maxSegmentSize = 10,而elementSize = 4,则向上取整后:segmentSize = 12;
const size_t maxSegmentSize =
opts.elementSize * (opts.maxSegmentSize / opts.elementSize);

// 每个段的字节数必须是每个元素的字节数的倍数才能进行缩减; 必要时四舍五入。
const size_t segmentBytes = roundUp(
std::min(
// Rounded division to have >= 2 segments per chunk.
(totalBytes + (context->size * 2 - 1)) / (context->size * 2),
// Configurable segment size limit
maxSegmentSize),
opts.elementSize);

// Compute how many segments make up the input buffer.
//
// 向上舍入到上下文大小的最接近的倍数,以便每个进程有相同数量的段,并且跨进程的执行是对称的。
// 最小值是上下文大小的两倍,因为下面的算法将发送/接收一个段与计算另一个段的缩减重叠。
//
const size_t numSegments = roundUp(
std::max(
(totalBytes + (segmentBytes - 1)) / segmentBytes,
(size_t)context->size * 2),
(size_t)context->size);
const size_t numSegmentsPerRank = numSegments / context->size;
const size_t chunkBytes = numSegmentsPerRank * segmentBytes;

// 分配暂存空间以容纳两个块
std::unique_ptr<uint8_t[]> tmpAllocation(new uint8_t[segmentBytes * 2]);
std::unique_ptr<transport::UnboundBuffer> tmpBuffer =
context->createUnboundBuffer(tmpAllocation.get(), segmentBytes * 2);
transport::UnboundBuffer* tmp = tmpBuffer.get();

// 使用动态查找临时缓冲区中的块偏移量。
// 在进行两个操作时,我们需要两个偏移量。
// 可以使用循环计数器对它们进行索引。
std::array<size_t, 2> segmentOffset;
segmentOffset[0] = 0;
segmentOffset[1] = segmentBytes;

// 函数计算给定块迭代要发送和接收的块的偏移量和长度。
auto computeReduceScatterOffsets = [&](size_t i) {
struct {
size_t sendOffset;
size_t recvOffset;
ssize_t sendLength;
ssize_t recvLength;
} result;

// 计算要发送的段索引(到 rank - 1)和要接收的段索引(从 rank + 1)。
// 乘以块中的字节数以获得偏移量。
// 允许偏移量超出范围(>=totalBytes),计算相关长度时会考虑到这一点。
result.sendOffset =
((((context->rank + 1) * numSegmentsPerRank) + i) * segmentBytes) %
(numSegments * segmentBytes);
result.recvOffset =
((((context->rank + 2) * numSegmentsPerRank) + i) * segmentBytes) %
(numSegments * segmentBytes);

// 如果段完全在范围内,则以下语句等于段字节。
// 如果不是,它会更少,甚至是负面的。 这就是需要 ssize_t 类型转换的原因。
result.sendLength = std::min(
(ssize_t)segmentBytes,
(ssize_t)totalBytes - (ssize_t)result.sendOffset);
result.recvLength = std::min(
(ssize_t)segmentBytes,
(ssize_t)totalBytes - (ssize_t)result.recvOffset);

return result;
};

for (auto i = 0; i < numSegments; i++) {
if (i >= 2) {
// 计算两次迭代前的发送和接收偏移量和长度。
// 需要这样我们知道何时等待操作以及何时忽略(当偏移量超出范围时),
// 并知道在哪里减少临时缓冲区的内容。
auto prev = computeReduceScatterOffsets(i - 2);
if (prev.recvLength > 0) {
tmp->waitRecv(opts.timeout);
opts.reduce(
static_cast<uint8_t*>(out->ptr) + prev.recvOffset,
static_cast<const uint8_t*>(in->ptr) + prev.recvOffset,
static_cast<const uint8_t*>(tmp->ptr) + segmentOffset[i & 0x1],
prev.recvLength / opts.elementSize);
}
if (prev.sendLength > 0) {
if ((i - 2) < numSegmentsPerRank) {
in->waitSend(opts.timeout);
} else {
out->waitSend(opts.timeout);
}
}
}

// 在最后两次迭代之外的所有迭代中发出新的发送和接收操作。
// 那时我们已经发送了我们需要的所有数据,只需要等待最终的段被减少到输出中。
if (i < (numSegments - 2)) {
// Compute send and receive offsets and lengths for this iteration.
auto cur = computeReduceScatterOffsets(i);
if (cur.recvLength > 0) {
tmp->recv(recvRank, slot, segmentOffset[i & 0x1], cur.recvLength);
}
if (cur.sendLength > 0) {
if (i < numSegmentsPerRank) {
in->send(sendRank, slot, cur.sendOffset, cur.sendLength);
} else {
out->send(sendRank, slot, cur.sendOffset, cur.sendLength);
}
}
}
}

// Gather to root rank.
// 注意:totalBytes <= (numSegments * segmentBytes),
// 这与在进程间贡献相同的通用聚集算法不兼容。
if (context->rank == opts.root) {
size_t numRecv = 0;
for (size_t rank = 0; rank < context->size; rank++) {
if (rank == context->rank) {
continue;
}
size_t recvOffset = rank * numSegmentsPerRank * segmentBytes;
ssize_t recvLength = std::min(
(ssize_t)chunkBytes, (ssize_t)totalBytes - (ssize_t)recvOffset);
if (recvLength > 0) {
out->recv(rank, slot, recvOffset, recvLength);
numRecv++;
}
}
for (size_t i = 0; i < numRecv; i++) {
out->waitRecv(opts.timeout);
}
} else {
size_t sendOffset = context->rank * numSegmentsPerRank * segmentBytes;
ssize_t sendLength = std::min(
(ssize_t)chunkBytes, (ssize_t)totalBytes - (ssize_t)sendOffset);
if (sendLength > 0) {
out->send(opts.root, slot, sendOffset, sendLength);
out->waitSend(opts.timeout);
}
}
}

1. 语言基础 (C/C++)

(0) 指针和引用的区别

  • 指针是一个新的变量,指向另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;而引用是一个别名,对引用的操作就是对变量的本身进行操作
  • 指针可以有多级,引用只有一级
  • 传参的时候,使用指针的话需要解引用才能对参数进行修改,而使用引用可以直接对参数进行修改
  • 指针的大小一般是4个字节,引用的大小取决于被引用对象的大小
  • 指针可以为空,引用不可以。

(1)在函数参数传递的时候,什么时候使用指针,什么时候使用引用?

  • 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
  • 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式

(2) 堆和栈有什么区别

  • 从定义上:堆是由new和malloc开辟的一块内存,由程序员手动管理,栈是编译器自动管理的内存,存放函数的参数和局部变量。
  • 堆空间因为会有频繁的分配释放操作,会产生内存碎片
  • 堆的生长空间向上,地址越来越大,栈的生长空间向下,地址越来越小

(3)堆快一点还是栈快一点?(字节提前批一面)

栈快一点。因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。

(4) new和delete是如何实现的,new 与 malloc的异同处

在new一个对象的时候,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数。delete会调用对象的析构函数,然后调用free回收内存。

new与malloc都会分配空间,但是new还会调用对象的构造函数进行初始化,malloc需要给定空间大小,而new只需要对象名

(5)既然有了malloc/free,C++中为什么还需要new/delete呢?

https://blog.csdn.net/leikun153/article/details/80612130

  • malloc/free和new/delete都是用来申请内存和回收内存的。
  • 在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free。

(6) C和C++的区别

包括但不限于:

  • C是面向过程的语言,C++是面向对象的语言,C++有“封装,继承和多态”的特性。封装隐藏了实现细节,使得代码模块化。继承通过子类继承父类的方法和属性,实现了代码重用。多态则是“一个接口,多个实现”,通过子类重写父类的虚函数,实现了接口重用。
  • C和C++内存管理的方法不一样,C使用malloc/free,C++除此之外还用new/delete
  • C++中还有函数重载和引用等概念,C中没有

(7)delete和delete[]的区别

  • delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

  • 用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

(8) C++、Java的联系与区别,包括语言特性、垃圾回收、应用场景等(java的垃圾回收机制)

包括但不限于:

  • C++ 和Java都是面向对象的语言,C++是编译成可执行文件直接运行的,JAVA是编译之后在JAVA虚拟机上运行的,因此JAVA有良好的跨平台特性,但是执行效率没有C++ 高。
  • C++的内存管理由程序员手动管理,JAVA的内存管理是由Java虚拟机完成的,它的垃圾回收使用的是标记-回收算法
  • C++有指针,Java没有指针,只有引用
  • JAVA和C++都有构造函数,但是C++有析构函数但是Java没有

(9)C++和python的区别

包括但不限于:

  1. python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。python可以很方便的跨平台,但是效率没有C++高。
  2. python使用缩进来区分不同的代码块,C++使用花括号来区分
  3. C++中需要事先定义变量的类型,而python不需要,python的基本数据类型只有数字,布尔值,字符串,列表,元组等等
  4. python的库函数比C++的多,调用起来很方便

(10) Struct和class的区别

  • 使用struct时,它的成员的访问权限默认是public的,而class的成员默认是private的
  • struct的继承默认是public继承,而class的继承默认是private继承
  • class可以用作模板,而struct不能

(11) define 和const的联系与区别(编译阶段、安全性、内存占用等)

联系:它们都是定义常量的一种方法。

区别:

  • define定义的常量没有类型,只是进行了简单的替换,可能会有多个拷贝,占用的内存空间大,const定义的常量是有类型的,存放在静态存储区,只有一个拷贝,占用的内存空间小。
  • define定义的常量是在预处理阶段进行替换,而const在编译阶段确定它的值。
  • define不会进行类型安全检查,而const会进行类型安全检查,安全性更高。
  • const可以定义函数而define不可以。

(12) 在C++中const的用法(定义,用途)

  • const修饰类的成员变量时,表示常量不能被修改
  • const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

(13) C++中的static用法和意义

static的意思是静态的,可以用来修饰变量,函数和类成员。

  • 变量:被static修饰的变量就是静态变量,它会在程序运行过程中一直存在,会被放在静态存储区。局部静态变量的作用域在函数体中,全局静态变量的作用域在这个文件里。

  • 函数:被static修饰的函数就是静态函数,静态函数只能在本文件中使用,不能被其他文件调用,也不会和其他文件中的同名函数冲突。

  • 类:而在类中,被static修饰的成员变量是类静态成员,这个静态成员会被类的多个对象共用。被static修饰的成员函数也属于静态成员,不是属于某个对象的,访问这个静态函数不需要引用对象名,而是通过引用类名来访问。

【note】静态成员函数要访问非静态成员时,要用过对象来引用。局部静态变量在函数调用结束后也不会被回收,会一直在程序内存中,直到该函数再次被调用,它的值还是保持上一次调用结束后的值。

注意和const的区别。const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象都共用。

(14) 计算下面几个类的大小:

1
2
3
4
5
6
7
class A {};
int main(){
cout<<sizeof(A)<<endl;// 输出 1;
A a;
cout<<sizeof(a)<<endl;// 输出 1;
return 0;
}

空类的大小是1, 在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。当该空白类作为基类时,该类的大小就优化为0了,子类的大小就是子类本身的大小。这就是所谓的空白基类最优化。

空类的实例大小就是类的大小,所以sizeof(a)=1字节,如果a是指针,则sizeof(a)就是指针的大小,即4字节。

1
2
3
4
5
6
7
class A { virtual Fun(){} };
int main(){
cout<<sizeof(A)<<endl;// 输出 4(32位机器)/8(64位机器);
A a;
cout<<sizeof(a)<<endl;// 输出 4(32位机器)/8(64位机器);
return 0;
}

因为有虚函数的类对象中都有一个虚函数表指针 __vptr,其大小是4字节

1
2
3
4
5
6
7
class A { static int a; };
int main(){
cout<<sizeof(A)<<endl;// 输出 1;
A a;
cout<<sizeof(a)<<endl;// 输出 1;
return 0;
}

静态成员存放在静态存储区,不占用类的大小, 普通函数也不占用类大小

1
2
3
4
5
6
7
class A { int a; };
int main(){
cout<<sizeof(A)<<endl;// 输出 4;
A a;
cout<<sizeof(a)<<endl;// 输出 4;
return 0;
}

1
2
3
4
5
6
7
class A { static int a; int b; };;
int main(){
cout<<sizeof(A)<<endl;// 输出 4;
A a;
cout<<sizeof(a)<<endl;// 输出 4;
return 0;
}

静态成员a不占用类的大小,所以类的大小就是b变量的大小 即4个字节

(15) C++的STL介绍(这个系列也很重要,建议侯捷老师的这方面的书籍与视频),其中包括内存管理allocator,函数,实现机理,多线程实现等

C++ STL从广义来讲包括了三类:算法,容器和迭代器。

  • 算法包括排序,复制等常用算法,以及不同容器特定的算法。
  • 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
  • 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

(16) STL源码中的hash表的实现

STL中的hash表就unordered_map。使用的是哈希进行实现(注意与map的区别)。它记录的键是元素的哈希值,通过对比元素的哈希值来确定元素的值。

unordered_map的底层实现是hashtable,采用开链法(也就是用桶)来解决哈希冲突,当桶的大小超过8时,就自动转为红黑树进行组织。

(17)解决哈希冲突的方式?

  1. 线性探查。该元素的哈希值对应的桶不能存放元素时,循序往后一一查找,直到找到一个空桶为止,在查找时也一样,当哈希值对应位置上的元素与所要寻找的元素不同时,就往后一一查找,直到找到吻合的元素,或者空桶。
  2. 二次探查。该元素的哈希值对应的桶不能存放元素时,就往后寻找1^2,2^2,3^2,4^2…..i^2个位置。
  3. 双散列函数法。当第一个散列函数发生冲突的时候,使用第二个散列函数进行哈希,作为步长。
  4. 开链法。在每一个桶中维护一个链表,由元素哈希值寻找到这个桶,然后将元素插入到对应的链表中,STL的hashtable就是采用这种实现方式。
  5. 建立公共溢出区。当发生冲突时,将所有冲突的数据放在公共溢出区。

(18) STL中unordered_map和map的区别

  • unordered_map是使用哈希实现的,占用内存比较多,查询速度比较快,是常数时间复杂度。它内部是无序的,需要实现==操作符。
  • map底层是采用红黑树实现的,插入删除查询时间复杂度都是O(log(n)),它的内部是有序的,因此需要实现比较操作符(<)。

(19) STL中vector的实现

STL中的vector是封装了动态数组的顺序容器。不过与动态数组不同的是,vector可以根据需要自动扩大容器的大小。具体策略是每次容量不够用时重新申请一块大小为原来容量两倍的内存,将原容器的元素拷贝至新容器,并释放原空间,返回新空间的指针。

在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。

(20) vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。

如果需要频繁插入,最好先指定vector的大小,因为vector在容器大小不够用的时候会重新申请一块大小为原容器两倍的空间,并将原容器的元素拷贝到新容器中,并释放原空间,这个过程是十分耗时和耗内存的。频繁调用push_back()会使得程序花费很多时间在vector扩容上,会变得很慢。这种情况可以考虑使用list。

(21)C++中vector和list的区别

vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。

vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。

list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等

vector::iterator和list::iterator都重载了“++”运算符。

总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;

如果需要大量的插入和删除,而不关心随机存取,则应使用list。

(22) C++中的重载和重写的区别:

  • 重载(overload)是指函数名相同,参数列表不同的函数实现方法。它们的返回值可以不同,但返回值不可以作为区分不同重载函数的标志。
  • 重写(overwide)是指函数名相同,参数列表相同,只有方法体不相同的实现方法。一般用于子类继承父类时对父类方法的重写。子类的同名方法屏蔽了父类方法的现象称为隐藏。

详见:https://blog.csdn.net/weixin_30379911/article/details/99497160

(23) C ++内存管理(热门问题)

https://blog.csdn.net/qq_43152052/article/details/98889139

在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。

  • 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 全局/静态存储区,内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据(局部static变量,全局static变量)、全局变量和常量。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量字符串,不允许修改。
  • 代码区,存放程序的二进制代码

关于这个有很多种说法,有的会增加一个自由存储区,存放malloc分配得到的内存,与堆相似。

(24) 介绍面向对象的三大特性,并且举例说明每一个。

面向对象的三大特性是:封装,继承和多态。

  • 封装隐藏了类的实现细节和成员数据,实现了代码模块化,如类里面的private和public;
  • 继承使得子类可以复用父类的成员和方法,实现了代码重用;
  • 多态则是“一个接口,多个实现”,通过父类调用子类的成员,实现了接口重用,如父类的指针指向子类的对象。

(25) 多态的实现(和下个问题一起回答)

C++ 多态包括编译时多态和运行时多态,编译时多态体现在函数重载和模板上,运行时多态体现在虚函数上。

  • 虚函数:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数.

(26) C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)

C++的虚函数是实现多态的机制。它是通过虚函数表实现的,虚函数表是每个类中存放虚函数地址的指针数组,类的实例在调用函数时会在虚函数表中寻找函数地址进行调用,如果子类覆盖了父类的函数,则子类的虚函数表会指向子类实现的函数地址,否则指向父类的函数地址。一个类的所有实例都共享同一张虚函数表。

详见:C++虚函数表剖析

  • 如果多重继承和多继承的话,子类的虚函数表长什么样子?
    多重继承的情况下越是祖先的父类的虚函数更靠前,多继承的情况下越是靠近子类名称的类的虚函数在虚函数表中更靠前。详见:https://blog.csdn.net/qq_36359022/article/details/81870219

(27) 实现编译器处理虚函数表应该如何处理

编译器处理虚函数的方法是:
如果类中有虚函数,就将虚函数的地址记录在类的虚函数表中。派生类在继承基类的时候,如果有重写基类的虚函数,就将虚函数表中相应的函数指针设置为派生类的函数地址,否则指向基类的函数地址。
为每个类的实例添加一个虚表指针(vptr),虚表指针指向类的虚函数表。实例在调用虚函数的时候,通过这个虚函数表指针找到类中的虚函数表,找到相应的函数进行调用。
详见:虚函数的作用及其底层实现机制

(28) 基类的析构函数一般写成虚函数的原因

首先析构函数可以为虚函数,当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找到子类的析构函数进行调用,从而正确释放子类对象的资源。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向子类的父类指针时,只会调用父类的析构函数而不调用子类析构函数,这样就会造成子类对象析构不完全造成内存泄漏。

(29) 构造函数为什么一般不定义为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

(30) 构造函数或者析构函数中调用虚函数会怎样

在构造函数中调用虚函数,由于当前对象还没有构造完成,此时调用的虚函数指向的是基类的函数实现方式。

在析构函数中调用虚函数,此时调用的是子类的函数实现方式。

(31) 纯虚函数

纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承

包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象

使用场景:当这个类本身产生一个实例没有意义的情况下,把这个类的函数实现为纯虚函数,比如动物可以派生出老虎兔子,但是实例化一个动物对象就没有意义。并且可以规定派生的子类必须重写某些函数的情况下可以写成纯虚函数。

(32) 静态绑定和动态绑定的介绍

C++中的静态绑定和动态绑定

静态绑定也就是将该对象相关的属性或函数绑定为它的静态类型,也就是它在声明的类型,在编译的时候就确定。在调用的时候编译器会寻找它声明的类型进行访问。

动态绑定就是将该对象相关的属性或函数绑定为它的动态类型,具体的属性或函数在运行期确定,通常通过虚函数实现动态绑定。

(33) 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)

浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源。

而深拷贝是新开辟一块空间,将原对象的资源复制到新的空间中,并返回该空间的地址。

深拷贝可以避免重复释放和写冲突。例如使用浅拷贝的对象进行释放后,对原对象的释放会导致内存泄漏或程序崩溃。

(34) 对象复用的了解,零拷贝的了解

对象复用指得是设计模式,对象可以采用不同的设计模式达到复用的目的,最常见的就是继承和组合模式了。

零拷贝指的是在进行操作时,避免CPU从一处存储拷贝到另一处存储。在Linux中,我们可以减少数据在内核空间和用户空间的来回拷贝实现,比如通过调用mmap()来代替read调用。

用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去。

(35) 介绍C++所有的构造函数

C++中的构造函数主要有三种类型:默认构造函数、重载构造函数和拷贝构造函数

  • 默认构造函数是当类没有实现自己的构造函数时,编译器默认提供的一个构造函数。
  • 重载构造函数也称为一般构造函数,一个类可以有多个重载构造函数,但是需要参数类型或个数不相同。可以在重载构造函数中自定义类的初始化方式。
  • 拷贝构造函数是在发生对象复制的时候调用的。

    (36) 什么情况下会调用拷贝构造函数(三种情况)

  • 对象以值传递的方式传入函数参数

    void func(Dog dog){};

  • 对象以值传递的方式从函数返回

    Dog func(){ Dog d; return d;}

  • 对象需要通过另外一个对象进行初始化

详见:C++拷贝构造函数详解

(37) 结构体内存对齐方式和为什么要进行内存对齐?

因为结构体的成员可以有不同的数据类型,所占的大小也不一样。同时,由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来。

对齐规则:

  • 第一个成员在与结构体变量偏移量为0的地址
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 对齐数=编译器默认的一个对齐数 与 该成员大小的较小值。
  • linux 中默认为4
  • vs 中的默认值为8
    结构体总大小为最大对齐数的整数倍(每个成员变量除了第一个成员都有一个对齐数)

(38) 内存泄露的定义,如何检测与避免?

动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

造成内存泄漏的几种原因:

1)类的构造函数和析构函数中new和delete没有配套

2)在释放对象数组时没有使用delete[],使用了delete

3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露

4)没有正确的清楚嵌套的对象指针

避免方法:

  1. malloc/free要配套
  2. 使用智能指针;
  3. 将基类的析构函数设为虚函数;

    (39) C++的智能指针有哪些

    C++中的智能指针有auto_ptr,shared_ptr,weak_ptr和unique_ptr。智能指针其实是将指针进行了封装,可以像普通指针一样进行使用,同时可以自行进行释放,避免忘记释放指针指向的内存地址造成内存泄漏。
  • auto_ptr是较早版本的智能指针,在进行指针拷贝和赋值的时候,新指针直接接管旧指针的资源并且将旧指针指向空,但是这种方式在需要访问旧指针的时候,就会出现问题。
  • unique_ptr是auto_ptr的一个改良版,不能赋值也不能拷贝,保证一个对象同一时间只有一个智能指针。
  • shared_ptr可以使得一个对象可以有多个智能指针,当这个对象所有的智能指针被销毁时就会自动进行回收。(内部使用计数机制进行维护)
  • weak_ptr是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。

    (40) 调试程序的方法

  • 通过设置断点进行调试
  • 打印log进行调试
  • 打印中间结果进行调试

    (41) 遇到coredump要怎么调试

    coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

  • 使用gdb命令对core文件进行调试

以下例子在Linux上编写一段代码并导致segment fault 并产生core文件

1
2
mkdir coredumpTest
vim coredumpTest.cpp

在编辑器内键入
1
2
3
4
5
6
7
#include<stdio.h>
int main(){
int i;
scanf("%d",i);//正确的应该是&i,这里使用i会导致segment fault
printf("%d\n",i);
return 0;
}

编译
1
g++ coredumpTest.cpp -g -o coredumpTest

运行
1
./coredumpTest

使用gdb调试coredump
1
gdb [可执行文件名] [core文件名]

(42) inline关键字说一下 和宏定义有什么区别

inline是内联的意思,可以定义比较小的函数。因为函数频繁调用会占用很多的栈空间,进行入栈出栈操作也耗费计算资源,所以可以用inline关键字修饰频繁调用的小函数。编译器会在编译阶段将代码体嵌入内联函数的调用语句块中。

1、内联函数在编译时展开,而宏在预编译时展开

2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。

3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。

4、宏不是函数,而inline是函数

5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

6、inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开。

7、宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。

(43) 模板的用法与适用场景 实现原理

用template \关键字进行声明,接下来就可以进行模板函数和模板类的编写了

编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,这次编译只会进行一个语法检查,并不会生成具体的代码。在运行时对代码进行参数替换后再进行编译,生成具体的函数代码。

(44) 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?

成员初始化列表就是在类或者结构体的构造函数中,在参数列表后以冒号开头,逗号进行分隔的一系列初始化字段。如下:

1
2
3
4
5
6
class A{
int id;
string name;
FaceImage face;
A(int& inputID,string& inputName,FaceImage& inputFace):id(inputID),name(inputName),face(inputFace){} // 成员初始化列表
};

因为使用成员初始化列表进行初始化的话,会直接使用传入参数的拷贝构造函数进行初始化,省去了一次执行传入参数的默认构造函数的过程,否则会调用一次传入参数的默认构造函数。所以使用成员初始化列表效率会高一些。

另外,有三种情况是必须使用成员初始化列表进行初始化的:

  • 常量成员的初始化,因为常量成员只能初始化不能赋值
  • 引用类型
  • 没有默认构造函数的对象必须使用成员初始化列表的方式进行初始化

详见C++ 初始化列表

(45) 用过C11吗,知道C11新特性吗?(有面试官建议熟悉C11)

  • 自动类型推导auto:auto的自动类型推导用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作
  • nullptr:nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,而nullptr是void*类型的

  • lambda表达式:它类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法如下:
    [函数对象参数](操作符重载函数参数)mutable或exception声明->返回值类型{函数体}

  • thread类和mutex类
  • 新的智能指针 unique_ptr和shared_ptr

(46) C++的调用惯例(简单一点C++函数调用的压栈过程)

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

(47) C++的四种强制转换

四种强制类型转换操作符分别为:static_cast、dynamic_cast、const_cast、reinterpret_cast

  • 1)static_cast :
    用于各种隐式转换。具体的说,就是用户各种基本数据类型之间的转换,比如把int换成char,float换成int等。以及派生类(子类)的指针转换成基类(父类)指针的转换。

    特性与要点:

    1. 它没有运行时类型检查,所以是有安全隐患的。
    2. 在派生类指针转换到基类指针时,是没有任何问题的,在基类指针转换到派生类指针的时候,会有安全问题。
    3. static_cast不能转换const,volatile等属性
  • 2)dynamic_cast:
    用于动态类型转换。具体的说,就是在基类指针到派生类指针,或者派生类到基类指针的转换。
    dynamic_cast能够提供运行时类型检查,只用于含有虚函数的类。
    dynamic_cast如果不能转换返回NULL。
  • 3)const_cast:
    用于去除const常量属性,使其可以修改 ,也就是说,原本定义为const的变量在定义后就不能进行修改的,但是使用const_cast操作之后,可以通过这个指针或变量进行修改; 另外还有volatile属性的转换。
  • 4)reinterpret_cast
    几乎什么都可以转,用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换等。但是不够安全。

    (48)string的底层实现

    string继承自basic_string,其实是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。

string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字符串拷贝过去,并加上新增的内容。

(49)一个函数或者可执行文件的生成过程或者编译过程是怎样的

预处理,编译,汇编,链接

  • 预处理: 对预处理命令进行替换等预处理操作
  • 编译:代码优化和生成汇编代码
  • 汇编:将汇编代码转化为机器语言
  • 链接:将目标文件彼此链接起来

    (50)set,map和vector的插入复杂度

    set,map的插入复杂度就是红黑树的插入复杂度,是log(N)。

unordered_set,unordered_map的插入复杂度是常数,最坏是O(N).

vector的插入复杂度是O(N),最坏的情况下(从头插入)就要对所有其他元素进行移动,或者扩容重新拷贝

(51)定义和声明的区别

  • 声明是告诉编译器变量的类型和名字,不会为变量分配空间

  • 定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次

    (52)typdef和define区别

define是预处理命令,在预处理是执行简单的替换,不做正确性的检查

typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名

(53)被free回收的内存是立即返还给操作系统吗?为什么

https://blog.csdn.net/YMY_mine/article/details/81180168

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

(54)引用作为函数参数以及返回值的好处

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(因为没有了传值和生成副本的时间和空间消耗)

如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

(55)友元函数和友元类

https://www.cnblogs.com/zhuguanhao/p/6286145.html

友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

有元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

using namespace std;

class A
{
public:
friend void set_show(int x, A &a); //该函数是友元函数的声明
private:
int data;
};

void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员
{
a.data = x;
cout << a.data << endl;
}
int main(void)
{
class A a;

set_show(1, a);

return 0;
}

一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
但是另一个类里面也要相应的进行声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

using namespace std;

class A
{
public:
friend class C; //这是友元类的声明
private:
int data;
};

class C //友元类定义,为了访问类A中的成员
{
public:
void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};

int main(void)
{
class A a;
class C c;

c.set_show(1, a);

return 0;
}

使用友元类时注意:

(1) 友元关系不能被继承。

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

(56) 说一下volatile关键字的作用

volatile的意思是“脆弱的”,表明它修饰的变量的值十分容易被改变,所以编译器就不会对这个变量进行优化(CPU的优化是让该变量存放到CPU寄存器而不是内存),进而提供稳定的访问。每次读取volatile的变量时,系统总是会从内存中读取这个变量,并且将它的值立刻保存。

(57) STL中的sort()算法是用什么实现的,stable_sort()呢

STL中的sort是用快速排序和插入排序结合的方式实现的,stable_sort()是归并排序。

(58)vector会迭代器失效吗?什么情况下会迭代器失效?

https://www.cnblogs.com/qingjiaowoxiaoxioashou/p/5874572.html

  • 当vector在插入的时候,如果原来的空间不够,会将申请新的内存并将原来的元素移动到新的内存,此时指向原内存地址的迭代器就失效了,first和end迭代器都失效
  • 当vector在插入的时候,end迭代器肯定会失效
  • 当vector在删除的时候,被删除元素以及它后面的所有元素迭代器都失效。

(58)为什么C++没有实现垃圾回收?

  • 首先,实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。
  • 垃圾回收会使得C++不适合进行很多底层的操作。

2. 计网相关

(1) 建立TCP服务器的各个系统调用

建立TCP服务器连接的过程中主要通过以下系统调用序列来获取某些函数,这些系统调用主要包括:socket(),bind(),listen(),accept(),send()和recv()。
详见:建立TCP 服务器的系统调用

(2) 继上一题,说明socket网络编程有哪些系统调用?其中close是一次就能直接关闭的吗,半关闭状态是怎么产生的?

socket()    创建套接字   
bind()      绑定本机端口    
connect()   建立连接     (TCP三次握手在调用这个函数时进行)
listen()    监听端口
accept()    接受连接
recv(), read(), recvfrom()  数据接收
send(), write(), sendto()   数据发送
close(), shutdown() 关闭套接字

使用close()时,只有当套接字的引用计数为0的时候才会终止连接,而用shutdown()就可以直接关闭连接

详见:网络编程Socket之TCP之close/shutdown详解

TCP连接与断开详解: https://www.cnblogs.com/felixzh/p/8359066.html

(3) 对路由协议的了解与介绍。内部网关协议IGP包括RIP,OSPF,和外部网关协议EGP和BGP.

  • RIP“路由信息协议(Route Information Protocol)”的简写,主要传递路由信息,通过每隔30秒广播一次路由表,维护相邻路由器的位置关系,同时根据收到的路由表信息使用动态规划的方式计算自己的路由表信息。RIP是一个距离矢量路由协议,最大跳数为16跳,16跳以及超过16跳的网络则认为目标网络不可达。

  • OSPF:详见:https://zhuanlan.zhihu.com/p/41341540

(4) UDP如何实现可靠传输

因为UDP是无连接的协议,所以在传输层上无法保证可靠传输,要想实现可靠传输,只能从应用层实现。需要实现seq/ack机制,重传机制和窗口确认机制。

就要接收方收到UDP之后回复个确认包,发送方有个机制,收不到确认包就要重新发送,每个包有递增的序号,接收方发现中间丢了包就要发重传请求,当网络太差时候频繁丢包,防止越丢包越重传的恶性循环,要有个发送窗口的限制,发送窗口的大小根据网络传输情况调整,调整算法要有一定自适应性。

作者:姚冬
链接:https://www.zhihu.com/question/283995548/answer/661809748
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(5) TCP和UDP的区别

  • TCP是面向连接的协议,提供的是可靠传输,在收发数据前需要通过三次握手建立连接,使用ACK对收发的数据进行正确性检验。而UDP是无连接的协议,不管对方有没有收到或者收到的数据是否正确。
  • TCP提供流量控制和拥塞控制,而UDP没有。
  • TCP对系统资源的要求高于UDP,所以速度也比UDP慢。
  • TCP数据包是没有边界的,会出现粘包的问题,UDP包是独立的,不会出现粘包问题。
  • 所以在应用方面,如果强调数据的完整性和正确性用TCP,当要求性能和速度的时候,使用UDP更加合适。

注:单凭TCP是不能保证完整性的,要是有黑客伪造TCP包,是无法识别的。

(6) TCP和UDP相关的协议与端口号

TCP族的协议有HTTP,HTTPS,SMTP,TelNet,FTP等,UDP族的协议有DNS,DHCP等等。
详见:https://blog.csdn.net/qq_22080999/article/details/81105051

(7) TCP(UDP,IP)等首部的认识(http请求报文构成)

TCP的头部大致包括:源端口,目的端口,序号,确认号,偏移位,标志位,校验和等等

UDP的头部则包括:源端口,目的端口,长度,校验和。

IP数据包的头部包括:源IP地址,目的IP地址,协议,校验和,总长度等等

详见:https://blog.csdn.net/zhangliangzi/article/details/52554439

(8) 网页解析的过程与实现方法

这里仅展示浏览器解析服务器响应的过程,URL解析和交互的完整过程在(9)

  • 首先是html文档解析,浏览器会将html文档生成解析树,也就是DOM树,它由dom元素以及属性节点组成。
  • 然后浏览器加载过程中如果遇到了外部css文件或者图片资源,还会另外发送请求来获取css文件和资源,这个请求通常是异步的,不会影响html文档的加载。
  • 不过如果浏览器在加载时遇到了js文件,则会挂起渲染的线程,等待js文件加载解析完毕才恢复html的渲染线程。
  • 然后是css解析,将css文件解析为样式表对象来渲染DOM树。

    (9) 在浏览器中输入URL后执行的全部过程(如www.baidu.com)

  1. 首先是域名解析,客户端使用DNS协议将URL解析为对应的IP地址;
  2. 然后建立TCP连接,客户端与服务器通过三次握手建立TCP连接;
  3. 接着是http连接,客户端向服务器发送http连接请求; (http连接无需额外连接,直接通过已经建立的TCP连接发送)
  4. 服务器对客户端发来的http请求进行处理,并返回响应;
  5. 客户端接收到http响应,将结果渲染展示给用户。

    (10) 网络层分片的原因与具体实现

    因为在链路层中帧的大小通常都有限制,比如在以太网中帧的最大大小(MTU)就是1500字节。如果IP数据包加上头部后大小超过1500字节,就需要分片。

IP分片和完整IP报文差不多拥有相同的IP头,16位ID域对于每个分片都是一致的,这样才能在重新组装的时候识别出来自同一个IP报文的分片。在IP头里面,16位识别号唯一记录了一个IP包的ID,具有同一个ID的IP分片将会重新组装;而13位片偏移则记录了某IP片相对整个包的位置;而这两个表中间的3位标志则标志着该分片后面是否还有新的分片。这三个标志就组成了IP分片的所有信息(将在后面介绍),接受方就可以利用这些信息对IP数据进行重新组织。
详见:https://blog.csdn.net/gettogetto/article/details/72851734

(11) TCP的三次握手与四次挥手的详细介绍(TCP连接建立与断开是热门问题)

  • 三次握手

第一次握手:首先client给server发送连接请求报文,在这个报文中,包含了SYN=1,client_seq=任意值i,发送之后处于SYN-SENT状态,这是第一次握手

第二次握手:server端接收到了这个请求,并分配资源,同时给client返回一个ACK报文,这个报文中呢包含了这些字段,标志位SYN和ACK都为1,而小ack为i+1,此时位于SYN-RCVD状态,这是第二次握手

第三次握手:client收到server发来的ACK信息后呢,他会看到server发过来的小ack是i+1,这时他知道了server收到了消息,也给server回一个ACK报文,报文中同样包含了ACK=1这样的消息,同时呢,还包括了client_ack=k+1这样的字段,这样呢三次握手之后,连接就建立了,client进入established(已建立连接)状态
三次握手.png

  • 四次挥手断开连接:

TCP断开连接通常是由一方主动,一方被动的,这里我们假设client主动,server被动
第一次挥手:当client没有数据要发送给server了,他会给server发送一个FIN报文,告诉server:“我已经没有数据要发给你了,但是你要是还想给我发数据的话,你就接着发,但是你得告诉我你收到我的关闭信息了”,这是第一次挥手,挥手之后client进入FIN_WAIT_1的第一阶段

第二次挥手:当server收到client发来的FIN报文后,告诉client:“我收到你的FIN消息了,但是你等我发完的”此时给client返回一个ACK信息,并且呢ack=seq+1,这是第二次挥手,挥手之后呢server进入CLOSE_WAIT阶段,而client收到之后处于FIN_WAIT_2第二阶段

第三次挥手:当server发完所有数据时,他会给client发送一个FIN报文,告诉client说“我传完数据了,现在要关闭连接了”,然后呢server变成LAST_ACK状态,等着client最后的ACK信息,这是第三次挥手

第四次挥手:当client收到这个FIN报文时,他会对这个消息进行确认,即给server发ACK信息,但是它不相信网络,怕server收不到信息,它会进入TIME_WAIT状态,万一server没收到ACK消息它可以可以重传,而当server收到这个ACK信息后,就正式关闭了tcp连接,处于CLOSED状态,而client等待了2MSL这样长时间后还没等到消息,它知道server已经关闭连接了,于是乎他自己也断开了,这是第四次挥手,这样tcp连接就断开了
fig/四次挥手.png

(12) TCP握手以及每一次握手客户端和服务器端处于哪个状态

见上

(13) 为什么使用三次握手,两次握手可不可以?

如果使用两次握手的话,三次握手中的最后一次缺失,服务器不能确认客户端的接收能力。

举两个例子,第一种是黑客会伪造大量SYN请求发送给服务器,服务器立即确认并建立连接,分配资源,但是这一系列连接并不是真实存在的,这大大浪费了服务器的资源并且阻塞了正常用户的连接,这种也叫SYN洪泛攻击。第二种是服务器返回给客户端的ACK数据包可能会在传输的过程中丢失,而客户端没有收到该ACK数据包而拒绝接收服务器接下来发送的数据,于是服务器一直在发送,客户端一直在拒绝,形成死锁。

(14) TIME_WAIT的意义(为什么要等于2MSL)

TIME_WAIT是指四次挥手中客户端接收了服务端的FIN报文并发送ACK报文给服务器后,仍然需要等待2MSL时间的过程。虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。如果客户端发送的ACK发生丢失,服务器会再次发送FIN报文给客户端,所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

(15) 超时重传机制(不太高频)

(16) TCP怎么保证可靠性?

(校序重流拥)

  • 校验和
    发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。

  • 确认应答+序列号
    TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

  • 超时重传
    当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

  • 流量控制
    TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。
    接收方有即时窗口(滑动窗口),随ACK报文发送

  • 拥塞控制
    当网络拥塞时,减少数据的发送。
    发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小

慢启动、拥塞避免、快速重传、快速恢复

(17) 流量控制的介绍,采用滑动窗口会有什么问题(死锁可能,糊涂窗口综合征)?

所谓流量控制就是让发送方发送速率不要过快,让接收方来得及接收。利用TCP报文段中的窗口大小字段来控制发送方的发送窗口不大于接收方发回的窗口大小就可以实施流量控制。

考虑一种特殊的情况,就是接收方若没有缓存足够使用,就会发送零窗口大小的报文,此时发送放将发送窗口设置为0,停止发送数据。之后接收方有足够的缓存,发送了非零窗口大小的报文,但是这个报文在中途丢失的,那么发送方的发送窗口就一直为零导致死锁。

解决这个问题,TCP为每一个连接设置一个持续计时器(persistence timer)。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小(注意:TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段)。

(18) tcp滑动窗口协议

详见 TCP-IP详解:滑动窗口SlidingWindowTCP滑动窗口

TCP的滑动窗口用来控制接收方和发送方的发送速率,避免拥塞的发生。滑动窗口其实就是接收端的缓冲区大小,用来告诉发送方对它发送的数据有多大的缓冲空间。在接收方的滑动窗口已知的情况下,当接收方确认了连续的数据序列之后,发送方的滑动窗口向后滑动,发送下一个数据序列。

接收方会在每个ACK数据包中附带自己当前的接受窗口(滑动窗口)的大小,方便发送方进行控制。

(19) 拥塞控制和流量控制的区别

拥塞控制是防止过多的数据注入到网络中,导致网络发生拥塞;而流量控制是防止发送方一下子发送过多的数据到接收方,导致接收方缓存放不下。两种算法都是对发送方的行为进行控制的。

(20) TCP拥塞控制,算法名字?(极其重要)

拥塞控制
防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载,拥塞控制自然也是控制发送者的流量,拥塞控制有四种算法,慢启动、拥塞避免,快速重传和快速恢复

发送方维持一个拥塞窗口 cwnd ( congestion window )的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口和接受窗口的较小值。

(1)慢启动。慢启动算法的思路是当主机开始发送数据时,先以比较小的拥塞窗口进行发送,然后每次翻倍,也就是说,由小到大逐渐增加拥塞窗口的大小,而这个大小是指数增长的,即1、2、4、8、16
*为了防止拥塞窗口cwnd增长过大引起网络拥塞,还要另外设置一个慢启动阈值ssthresh状态变量,当拥塞窗口的大小超过慢启动阈值的时候( cwnd > ssthresh 时),停止使用慢开始算法而改用拥塞避免算法

(2)拥塞避免。拥塞避免算法的思路是让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。

(3)快速重传。当发送端连续收到三个重复的ack时,表示该数据段已经丢失,需要重发。此时慢启动阈值ssth变为原来一半,拥塞窗口cwnd变为ssth+3,然后+1+1的发(每一轮rtt+1)

(4)快速恢复。当超过设定的时间没有收到某个报文段的ack时,表示网络拥塞,慢启动阈值ssth变为原来一半,拥塞窗口cwnd=1,进入慢启动阶段

(21) http协议与TCP的区别与联系

联系:Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据传输完毕后,Http会立即将TCP连接断开,这个过程是很短的。

区别:HTTP和TCP位于不同的网络分层。TCP是传输层的协议,定义的是数据传输和连接的规范,而HTTP是应用层的,定义的是数据的内容的规范。
建立一个TCP请求需要进行三次握手,而由于http是建立在tcp连接之上的,建立一个http请求通常包含请求和响应两个步骤。

(22) http/1.0和http/1.1的区别

HTTP 协议老的标准是 HTTP/1.0 ,目前最通用的标准是 HTTP/1.1 。
HTTP1.0 只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个 TCP 连接,但是最新的http/1.0加入了长连接,只需要在客户端给服务器发送的http报文头部加入Connection:keep-alive
HTTP 1.1 支持持久连接,默认进行持久连接,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。

(23) http的请求方法有哪些?get和post的区别。

HTTP的请求方法包括GET,POST,PUT,DELETE四种基本方法。(四种方法中只有POST不是操作幂等性的)

get和post的区别:

  1. get方法不会修改服务器上的资源,它的查询是没有副作用的,而post有可能会修改服务器上的资源
  2. get可以保存为书签,可以用缓存来优化,而post不可以
  3. get把请求附在url上,而post把参数附在http包的包体中
  4. 浏览器和服务器一般对get方法所提交的url长度有限制,一般是1k或者2k,而对post方法所传输的参数大小限制为80k到4M不等
  5. post可以传输二进制编码的信息,get的参数一般只支持ASCII

    (24) http的状态码 403 201等等是什么意思

    详见 HTTP状态码的含义

常见的状态码有:

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误
  • 400 - 请求无效
  • 403 - 禁止访问

    (25) http和https的区别,由http升级为https需要做哪些操作

    http 是超文本传输协议,信息是明文传输, https 则是具有安全性的 ssl 加密传输协议
    http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80 ,后者是 443
    http 的连接很简单,是无状态的; HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比http 协议安全。
    https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用
    https://www.cnblogs.com/wqhwe/p/5407468.html

(26) https的具体实现,怎么确保安全性

SSL是传输层的协议

https包括非对称加密和对称加密两个阶段,在客户端与服务器建立连接的时候使用非对称加密,连接建立以后使用的是对称加密。

  1. 客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接
  2. Web服务器收到客户端请求后,会将网站的公钥传送一份给客户端,私钥自己保存。
  3. 客户端的浏览器根据双方同意的安全等级,生成对称加密使用的密钥,称为会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站
  4. Web服务器利用自己的私钥解密出会话密钥。
  5. Web服务器利用会话密钥加密与客户端之间的通信,这个过程是对称加密的过程。

服务器第一次传给客户端的公钥其实是CA对网站信息进行加密的数字证书

客户端的对称加密密钥其实是三个随机数的哈希(1. 客户端第一次给服务端发送请求时附带的随机数 2. 服务器返回时的随机数 3. 客户端收到返回时的随机数)

(27) TCP三次握手时的第一次的seq序号是怎样产生的

第一次的序号是随机序号,但也不是完全随机,它是使用一个ISN算法得到的。

seq = C + H (源IP地址,目的IP地址,源端口,目的端口)。其中,C是一个计时器,每隔一段时间值就会变大,H是消息摘要算法,输入是一个四元组(源IP地址,目的IP地址,源端口,目的端口)。

(28) 一个机器能够使用的端口号上限是多少,为什么?可以改变吗?那如果想要用的端口超过这个限制怎么办?

65536.因为TCP的报文头部中源端口号和目的端口号的长度是16位,也就是可以表示2^16=65536个不同端口号,因此TCP可供识别的端口号最多只有65536个。但是由于0到1023是知名服务端口,所以实际上还要少1024个端口号。

而对于服务器来说,可以开的端口号与65536无关,其实是受限于Linux可以打开的文件数量,并且可以通过MaxUserPort来进行配置。

(29) 对称密码和非对称密码体系

https://blog.csdn.net/qq_29689487/article/details/81634057

  • 对称加密:加密和解密使用的密钥是同一个
    • 优点:计算量小,算法速度快,加密效率高 缺点:密钥容易泄漏。不同的会话需要不同的密钥,管理起来很费劲
    • 常用算法:DES,3DES,IDEA,CR4,CR5,CR6,AES
  • 非对称加密:需要公钥和私钥,公钥用来加密,私钥用来解密
    • 优点:安全,不怕泄漏 缺点:速度慢
    • 常用算法:RSA,ECC,DSA

      (30) 数字证书的了解(高频)

      fig/数字证书.jpg

权威CA使用私钥将网站A的信息和消息摘要(签名S)进行加密打包形成数字证书。公钥给客户端。

网站A将自己的信息和数字证书发给客户端,客户端用CA的公钥对数字证书进行解密,得到签名S,与手动将网站的信息进行消息摘要得到的结果S*进行对比,如果签名一致就证明网站A可以信任。

(31) 服务器出现大量close_wait的连接的原因以及解决方法

close_wait状态是在TCP四次挥手的时候收到FIN但是没有发送自己的FIN时出现的,服务器出现大量close_wait状态的原因有两种:

  • 服务器内部业务处理占用了过多时间,都没能处理完业务;或者还有数据需要发送;或者服务器的业务逻辑有问题,没有执行close()方法
  • 服务器的父进程派生出子进程,子进程继承了socket,收到FIN的时候子进程处理但父进程没有处理该信号,导致socket的引用不为0无法回收

处理方法:

  • 停止应用程序
  • 修改程序里的bug

    (32) 消息摘要算法列举一下,介绍MD5算法,为什么MD5是不可逆的,有什么办法可以加强消息摘要算法的安全性让它不那么容易被破解呢?(百度安全一面)

  • 消息摘要算法有MD家族(MD2,MD4,MD5),SHA家族(SHA-1,SHA-256)和CRC家族(CRC8,CRC16,CRC32)等等

  • MD5算法介绍:
    MD5以512位分组来处理输入的信息,且每一分组又被划分为若干个小分组(16个32位子分组),经过一些列的处理后,算法输出由四个散列值(32位分组组成的128位散列值。)

  1. MD5首先将输入的信息分成若干个512字节长度的分组,如果不够就填充1和若干个0。
  2. 对每个512字节的分组进行循环运算。使用四个幻数对第一个分组的数据进行四轮变换,得到四个变量。
  3. 接下来对其中三个使用线性函数进行计算,与剩下一个相加,并赋值给其中某个变量,得到新的四个变量,重复16次这个过程,得到的四个变量作为幻数,与下一个分组进行相似的计算。
  4. 遍历所有分组后得到的四个变量即为结果。

详见:https://blog.csdn.net/weixin_39640298/article/details/84555814

  • 为什么不可逆:因为MD5在进行消息摘要的过程中,数据与原始数据相比发生了丢失,所以不能由结果进行恢复。

  • 加强安全性:加盐(加随机数)

    (33) 单条记录高并发访问的优化

    服务器端:

  • 使用缓存,如redis等
  • 使用分布式架构进行处理
  • 将静态页面和静态资源存储在静态资源服务器,需要处理的数据使用服务器进行计算后返回
  • 将静态资源尽可能在客户端进行缓存
  • 采用ngnix进行负载均衡 (nginx读作恩静埃克斯 = Engine X)

数据库端:

  • 数据库采用主从赋值,读写分离措施
  • 建立适当的索引
  • 分库分表

    (34) 介绍一下ping的过程,分别用到了哪些协议

    详见:Ping原理与ICMP协议

ping是使用ICMP协议来进行工作的。 ICMP:网络控制报文协议

  • 首先,ping命令会构建一个ICMP请求数据包,然后由ICMP协议将这个数据包连同目的IP地址源IP地址一起交给IP协议。
  • 然后IP协议就会构建一个IP数据报,并且在映射表中查找目的IP对应的mac地址,将其交给数据链路层。
  • 然后数据链路层就会构建一个数据帧,附上源mac地址和目的mac地址发送出去。

目的主机接收到数据帧后,就会检查包上的mac地址与本机mac是否相符,如果相符,就接收并把其中的信息提取出来交给IP协议,IP协议就会将其中的信息提取出来交给ICMP协议。然后构建一个ICMP应答包,用相同的过程发送回去。

(35) TCP/IP的粘包与避免介绍一下

因为TCP为了减少额外开销,采取的是流式传输,所以接收端在一次接收的时候有可能一次接收多个包。而TCP粘包就是发送方的若干个数据包到达接收方的时候粘成了一个包。多个包首尾相接,无法区分。

导致TCP粘包的原因有三方面:

  • 发送端等待缓冲区满才进行发送,造成粘包
  • 接收方来不及接收缓冲区内的数据,造成粘包
  • 由于TCP协议在发送较小的数据包的时候,会将几个包合成一个包后发送

避免粘包的措施:

  • 通过编程,强制使TCP发生数据传送,不必等到缓冲区满
  • 优化接收方接收数据的过程,使其来得及接收数据包,包括提高接收进程优先级等
  • 设置固定长度的报文或者设置报文头部指示报文的长度。

(36) 说一下TCP的封包和拆包

因为TCP是无边界的流传输,所以需要对TCP进行封包和拆包,确保发送和接收的数据不粘连。

  • 封包:封包就是在发送数据报的时候为每个TCP数据包加上一个包头,将数据报分为包头和包体两个部分。包头是一个固定长度的结构体,里面包含该数据包的总长度。
  • 拆包:接收方在接收到报文后提取包头中的长度信息进行截取。

    (37) 一个ip配置多个域名,靠什么识别?

  • 靠host主机名区分
  • 靠端口号区分

    (38) 服务器攻击(DDos攻击)

    (39)DNS的工作过程和原理


    DNS解析有两种方式:递归查询和迭代查询
  • 递归查询 用户先向本地域名服务器查询,如果本地域名服务器的缓存没有IP地址映射记录,就向根域名服务器查询,根域名服务器就会向顶级域名服务器查询,顶级域名服务器向权限域名服务器查询,查到结果后依次返回。
  • 迭代查询 用户向本地域名服务器查询,如果没有缓存,本地域名服务器会向根域名服务器查询,根域名服务器返回顶级域名服务器的地址,本地域名服务器再向顶级域名服务器查询,得到权限域名服务器的地址,本地域名服务器再向权限域名服务器查询得到结果

    (41)OSA七层协议和五层协议,分别有哪些

    OSI七层协议模型主要是:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。

五层体系结构包括:应用层、传输层、网络层、数据链路层和物理层。

(fig/网络协议层.png

(42)IP寻址和MAC寻址有什么不同,怎么实现的

通过MAC地址寻找主机是MAC地址寻址,通过IP地址寻找主机叫IP地址寻址。它们适用于不同的协议层,IP寻址是网络层,Mac寻址是数据链路层。

http://c.biancheng.net/view/6388.html

https://blog.csdn.net/wxy_nick/article/details/9190693

IP寻址的过程(ARP协议):主机A想通过IP地址寻找到目标主机,首先分析IP地址确定目标主机与自己是否为同一网段。如果是则查看ARP缓存,或者使用ARP协议发送广播。如果不是,则寻找网关发送ARP数据包

3. 数据库

(1) 关系型和非关系型数据库的区别(低频)

  • 关系型数据库的优点
    1. 容易理解。因为它采用了关系模型来组织数据。
    2. 可以保持数据的一致性。
    3. 数据更新的开销比较小。
    4. 支持复杂查询(带where子句的查询)
  • 非关系型数据库的优点
    1. 不需要经过sql层的解析,读写效率高。
    2. 基于键值对,数据的扩展性很好。
    3. 可以支持多种类型数据的存储,如图片,文档等等。

      (2) 什么是非关系型数据库(低频)

      非关系型数据库也叫nosql,采用键值对的形式进行存储。它的读写性能很高,易于扩展。例如Redis,Mongodb,hbase等等。

适合使用非关系型数据库的场景:

  • 日志系统
  • 地理位置存储
  • 数据量巨大
  • 高可用

    (3) 说一下 MySQL 执行一条查询语句的内部执行过程?

  • 连接器:客户端先通过连接器连接到 MySQL 服务器。
  • 缓存:连接器权限验证通过之后,先查询是否有查询缓存,如果有缓存(之前执行过此语句)则直接返回缓存数据,如果没有缓存则进入分析器。
  • 分析器:分析器会对查询语句进行语法分析和词法分析,判断 SQL 语法是否正确,如果查询语法错误会直接返回给客户端错误信息,如果语法正确则进入优化器。
  • 优化器:优化器是对查询语句进行优化处理,例如一个表里面有多个索引,优化器会判别哪个索引性能更好。
  • 执行器:优化器执行完就进入执行器,执行器就开始执行语句进行查询比对了,直到查询到满足条件的所有数据,然后进行返回。

    (4) 数据库的索引类型

    数据库的索引类型分为逻辑分类和物理分类

    逻辑分类:
  • 主键索引 当关系表中定义主键时会自动创建主键索引。每张表中的主键索引只能有一个,要求主键中的每个值都唯一,即不可重复,也不能有空值。
  • 唯一索引 数据列不能有重复,可以有空值。一张表可以有多个唯一索引,但是每个唯一索引只能有一列。如身份证,卡号等。
  • 普通索引 一张表可以有多个普通索引,可以重复可以为空值
  • 全文索引 可以加快模糊查询,不常用

物理分类:

  • 聚集索引(聚簇索引) 数据在物理存储中的顺序跟索引中数据的逻辑顺序相同,比如以ID建立聚集索引,数据库中id从小到大排列,那么物理存储中该数据的内存地址值也按照从小到大存储。一般是表中的主键索引,如果没有主键索引就会以第一个非空的唯一索引作为聚集索引。一张表只能有一个聚集索引。
  • 非聚集索引 数据在物理存储中的顺序跟索引中数据的逻辑顺序不同。非聚集索引因为无法定位数据所在的行,所以需要扫描两遍索引树。第一遍扫描非聚集索引的索引树,确定该数据的主键ID,然后到主键索引(聚集索引)中寻找相应的数据。

    (5) 说一下事务是怎么实现的

    https://blog.csdn.net/u013256816/article/details/103966510

https://www.cnblogs.com/takumicx/p/9998844.html

事务就是一组逻辑操作的集合。实现事务就是要保证可靠性和并发隔离,或者说,能够满足ACID特性的机制。而这些主要是靠日志恢复和并发控制实现的。

  • 日志恢复:数据库里有两个日志,一个是redo log,一个是undo log。redo log记录的是已经成功提交的事务操作信息,用来恢复数据,保证事务的持久性。undo log记录的是事务修改之前的数据信息,用来回滚数据,保证事务的原子性
  • 并发控制:并发控制主要靠读写锁和MVCC(多版本并发控制)来实现。读写锁包括共享锁和排他锁,保证事务的隔离性。MVCC通过为数据添加时间戳来实现。

(6) MySQL怎么建立索引,怎么建立主键索引,怎么删除索引?

MySQL建立索引有两种方式:用alter table或者create index。

1
2
3
alter table table_name add primary key(column_list) #添加一个主键索引
alter table table_name add index (column_list) #添加一个普通索引
alter table table_name add unique (column_list) #添加一个唯一索引

1
2
create index index_name on table_name (column_list)   #创建一个普通索引
create unique index_name on table_name (column_list) #创建一个唯一索引

Mysql删除索引同样也有两种方式:alter table 和 drop index

1
2
alter table table_name drop index index_name    #删除一个普通索引
alter table table_name drop primary key #删除一个主键索引

1
drop index index_name on table table_name

(7) 索引的优缺点,什么时候使用索引,什么时候不能使用索引(重点)

https://www.cnblogs.com/wezheng/p/8399305.html

  • 经常搜索的列上建索引
  • 作为主键的列上要建索引
  • 经常需要连接(where子句)的列上
  • 经常需要排序的列
  • 经常需要范围查找的列

哪些列不适合建索引?

  • 很少查询的列
  • 更新很频繁的列
  • 数据值的取值比较少的列(比如性别)

    (8) 索引的底层实现(重点)

    数据库的索引是使用B+树来实现的。

(为什么要用B+树,为什么不用红黑树和B树)

B+树是一种特殊的平衡多路树,是B树的优化改进版本,它把所有的数据都存放在叶节点上,中间节点保存的是索引。这样一来相对于B树来说,减少了数据对中间节点的空间占用,使得中间节点可以存放更多的指针,使得树变得更矮,深度更小,从而减少查询的磁盘IO次数,提高查询效率。另一个是由于叶节点之间有指针连接,所以可以进行范围查询,方便区间访问。

而红黑树是二叉的,它的深度相对B+树来说更大,更大的深度意味着查找次数更多,更频繁的磁盘IO,所以红黑树更适合在内存中进行查找。

(9) B树和B+树的区别(重点)

./fig/Bptree.png

这都是由于B+树和B具有不同的存储结构所造成的区别,以一个m阶树为例。

  1. 关键字的数量不同;B+树中分支结点有m个关键字,其叶子结点也有m个,其关键字只是起到了一个索引的作用,但是B树虽然也有m个子结点,但是其只拥有m-1个关键字。
  2. 存储的位置不同;B+树中的数据都存储在叶子结点上,也就是其所有叶子结点的数据组合起来就是完整的数据,但是B树的数据存储在每一个结点中,并不仅仅存储在叶子结点上。
  3. 分支结点的构造不同;B+树的分支结点仅仅存储着关键字信息和儿子的指针(这里的指针指的是磁盘块的偏移量),也就是说内部结点仅仅包含着索引信息。
  4. 查询不同;B树在找到具体的数值以后,则结束,而B+树则需要通过索引找到叶子结点中的数据才结束,也就是说B+树的搜索过程中走了一条从根结点到叶子结点的路径。

B+树优点:由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引,而B树则常用于文件索引。

(10) 索引最左前缀/最左匹配

假如我们对a b c三个字段建立了联合索引,在联合索引中,从最左边的字段开始,任何连续的索引都能匹配上,当遇到范围查询的时候停止。比如对于联合索引index(a,b,c),能匹配a,ab,abc三组索引。并且对查询时字段的顺序没有限制,也就是a,b,c; b,a,c; c,a,b; c,b,a都可以匹配。

(11) Mysql的优化(高频,索引优化,性能优化)

高频访问:

  • 分表分库:将数据库表进行水平拆分,减少表的长度
  • 增加缓存: 在web和DB之间加上一层缓存层
  • 增加数据库的索引:在合适的字段加上索引,解决高频访问的问题

并发优化:

  • 主从读写分离:只在主服务器上写,从服务器上读
  • 负载均衡集群:通过集群或者分布式的方式解决并发压力

    (12) MYSQL数据库引擎介绍,innodb和myisam的特点与区别

  • InnoDB : InnoDB是mysql的默认引擎,支持事务和外键,支持容灾恢复。适合更新频繁和多并发的表 行级锁
  • MyISAM : 插入和查询速度比较高,支持大文件,但是不支持事务,适合在web和数据仓库场景下使用 表级锁
  • MEMORY : memory将表中的数据保存在内存里,适合数据比较小而且频繁访问的场景
  • CSV
  • blackhole

    (13) 数据库中事务的ACID(四大特性都要能够举例说明,理解透彻,比如原子性和一致性的关联,隔离性不好会出现的问题)

    数据库事务是指逻辑上对数据的一种操作,这个事务要么全部成功,要么全部失败。

A: atom 原子性

数据库事务的原子性是指:事务是一个不可分割的工作单位,这组操作要么全部发生,要么全部不发生。

C: consistency 一致性

数据库事务的一致性是指:在事务开始以前,数据库中的数据有一个一致的状态。在事务完成后,数据库中的事务也应该保持这种一致性。事务应该将数据从一个一致性状态转移到另一个一致性状态。
比如在银行转账操作后两个账户的总额应当不变。

I: isolation 隔离性

数据库事务的隔离性要求数据库中的事务不会受另一个并发执行的事务的影响,对于数据库中同时执行的每个事务来说,其他事务要么还没开始执行,要么已经执行结束,它都感觉不到还有别的事务正在执行。

D:durability 持久性

数据库事务的持久性要求事务对数据库的改变是永久的,哪怕数据库发生损坏都不会影响到已发生的事务。
如果事务没有完成,数据库因故断电了,那么重启后也应该是没有执行事务的状态,如果事务已经完成后数据库断电了,那么重启后就应该是事务执行完成后的状态。

(14)什么是脏读,不可重复读和幻读?

详见数据库的事务隔离级别总结

  • 脏读:脏读是指一个事务在处理过程中读取了另一个还没提交的事务的数据。

    比如A向B转账100,A的账户减少了100,而B的账户还没来得及修改,此时一个并发的事务访问到了B的账户,就是脏读

  • 不可重复读:不可重复读是对于数据库中的某一个字段,一个事务多次查询却返回了不同的值,这是由于在查询的间隔中,该字段被另一个事务修改并提交了。

    比如A第一次查询自己的账户有1000元,此时另一个事务给A的账户增加了1000元,所以A再次读取他的账户得到了2000的结果,跟第一次读取的不一样。
    不可重复读与脏读的不同之处在于,脏读是读取了另一个事务没有提交的脏数据,不可重复读是读取了已经提交的数据,实际上并不是一个异常现象。

  • 幻读:事务多次读取同一个范围的时候,查询结果的记录数不一样,这是由于在查询的间隔中,另一个事务新增或删除了数据。

    比如A公司一共有100个人,第一次查询总人数得到100条记录,此时另一个事务新增了一个人,所以下一次查询得到101条记录。
    不可重复度和幻读的不同之处在于,幻读是多次读取的结果行数不同,不可重复度是读取结果的值不同。

避免不可重复读需要锁行,避免幻读则需要锁表。

脏读,不可重复读和幻读都是数据库的读一致性问题,是在并行的过程中出现的问题,必须采用一定的隔离级别解决。
详见脏读、不可重复读和幻读的区别

(15) 数据库的隔离级别,mysql和Oracle的隔离级别分别是什么(重点)

详见数据库的事务隔离级别总结数据库隔离级别

为了保证数据库事务一致性,解决脏读,不可重复读和幻读的问题,数据库的隔离级别一共有四种隔离级别:

  • 读未提交 Read Uncommitted: 最低级别的隔离,不能解决以上问题
  • 读已提交 Read committed: 可以避免脏读的发生
  • 可重复读 Reapeatable read: 确保事务可以多次从一个字段中读取相同的值,在该事务执行期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读。 通过锁行来实现
  • 串行化 Serializaion 最严格的事务隔离机制,要求所有事务被串行执行,可以避免以上所有问题。 通过锁表来实现

Oracle的默认隔离级别是读已提交,实现了四种隔离级别中的读已提交和串行化隔离级别

MySQL的默认隔离级别是可重复读,并且实现了所有四种隔离级别

(16) 数据库连接池的作用

(17) Mysql的表空间方式,各自特点

  • 共享表空间:指的是数据库的所有的表数据,索引文件全部放在一个文件中,默认这个共享表空间的文件路径在 data 目录下。
  • 独立表空间:每一个表都将会生成以独立的文件方式来进行存储。 优点:当表被删除时这部分空间可以被回收;可以更快的恢复和备份单个表;将单个表复制到另一个实例会很方便; 缺点:mysqld会维持很多文件句柄,表太多会影响性能。如果很多表都增长会导致碎片问题

    (18) 分布式事务

    (19) 数据库的范式

    https://www.cnblogs.com/linjiqin/archive/2012/04/01/2428695.html

  • 第一范式(确保每列保持原子性)

    第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。

比如 学生 选课(包括很多课程) 就不符合第一范式

  • 第二范式(确保表中的每列都和主键相关)

    在满足第一范式的前提下,(主要针对联合主键而言)第二范式需要确保数据库表中的每一列都和主键的所有成员直接相关,由整个主键才能唯一确定,而不能只与主键的某一部分相关或者不相关。

比如一张学生信息表,由主键(学号)可以唯一确定一个学生的姓名,班级,年龄等信息。但是主键 (学号,班级) 与列 姓名,班主任,教室 就不符合第二范式,因为班主任跟部分主键(班级)是依赖关系

  • 第三范式(确保非主键的列没有传递依赖)

    在满足第二范式的前提下,第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。非主键的列不能确定其他列,列与列之间不能出现传递依赖。

比如一张学生信息表,主键是(学号)列包括 姓名,班级,班主任 就不符合第三范式,因为非主键的列中 班主任 依赖于 班级

  • BCNF范式(确保主键之间没有传递依赖)

    主键有可能是由多个属性组合成的复合主键,那么多个主键之间不能有传递依赖。也就是复合主键之间谁也不能决定谁,相互之间没有关系。

    (20) 数据的锁的种类,加锁的方式

    以MYSQL为例,
  • 按照类型来分有乐观锁和悲观锁
  • 根据粒度来分有行级锁,页级锁,表级锁(粒度一个比一个大) (仅BDB,Berkeley Database支持页级锁)
  • 根据作用来分有共享锁(读锁)和排他锁(写锁)。

    (21) 什么是共享锁和排他锁

  • 共享锁是读操作的时候创建的锁,一个事务对数据加上共享锁之后,其他事务只能对数据再加共享锁,不能进行写操作直到释放所有共享锁。
  • 排他锁是写操作时创建的锁,事务对数据加上排他锁之后其他任何事务都不能对数据加任何的锁(即其他事务不能再访问该数据)

https://blog.csdn.net/qq_42743933/article/details/81236658

(22) 分库分表的理解和简介

(23)

(24)数据库高并发的解决方案

  1. 在web服务框架中加入缓存。在服务器与数据库层之间加入缓存层,将高频访问的数据存入缓存中,减少数据库的读取负担。
  2. 增加数据库索引。提高查询速度。(不过索引太多会导致速度变慢,并且数据库的写入会导致索引的更新,也会导致速度变慢)
  3. 主从读写分离,让主服务器负责写,从服务器负责读。
  4. 将数据库进行拆分,使得数据库的表尽可能小,提高查询的速度。
  5. 使用分布式架构,分散计算压力。

    (25)乐观锁与悲观锁解释一下

    一般的数据库都会支持并发操作,在并发操作中为了避免数据冲突,所以需要对数据上锁,乐观锁和悲观锁就是两种不同的上锁方式。

悲观锁假设数据在并发操作中一定会发生冲突,所以在数据开始读取的时候就把数据锁住。而乐观锁则假设数据一般情况下不会发生冲突,所以在数据提交更新的时候,才会检测数据是否有冲突。

(26)乐观锁与悲观锁是怎么实现的

悲观锁有行级锁和页级锁两种形式。行级锁对正在使用的单条数据进行锁定,事务完成后释放该行数据,而页级锁则对整张表进行锁定,事务正在对该表进行访问的时候不允许其他事务并行访问。

悲观锁要求在整个过程中一直与数据库有一条连接,因为上一个事务完成后才能让下一个事务执行,这个过程是串行的。

乐观锁有三种常用的实现形式:

  • 一种是在执行事务时把整个数据都拷贝到应用中,在数据更新提交的时候比较数据库中的数据与新数据,如果两个数据一摸一样则表示没有冲突可以直接提交,如果有冲突就要交给业务逻辑去解决。
  • 一种是使用版本戳来对数据进行标记,数据每发生一次修改,版本号就增加1。某条数据在提交的时候,如果数据库中的版本号与自己的一致,就说明数据没有发生修改,否则就认为是过期数据需要处理。
  • 最后一种采用时间戳对数据最后修改的时间进行标记。与上一种类似。

4. Linux

(1) Linux的I/O模型介绍以及同步异步阻塞非阻塞的区别(超级重要)

https://blog.csdn.net/sqsltr/article/details/92762279

https://www.cnblogs.com/euphie/p/6376508.html

(IO过程包括两个阶段:(1)内核从IO设备读写数据和(2)进程从内核复制数据)

  • 阻塞:调用IO操作的时候,如果缓冲区空或者满了,调用的进程或者线程就会处于阻塞状态直到IO可用并完成数据拷贝。
  • 非阻塞:调用IO操作的时候,内核会马上返回结果,如果IO不可用,会返回错误,这种方式下进程需要不断轮询直到IO可用为止,但是当进程从内核拷贝数据时是阻塞的。
  • IO多路复用就是同时监听多个描述符,一旦某个描述符IO就绪(读就绪或者写就绪),就能够通知进程进行相应的IO操作,否则就将进程阻塞在select或者epoll语句上。
  • 同步IO:同步IO模型包括阻塞IO,非阻塞IO和IO多路复用。特点就是当进程从内核复制数据的时候都是阻塞的。
  • 异步IO:在检测IO是否可用和进程拷贝数据的两个阶段都是不阻塞的,进程可以做其他事情,当IO完成后内核会给进程发送一个信号。

    (2) 文件系统的理解(EXT4,XFS,BTRFS)

(3) EPOLL的介绍和了解

https://zhuanlan.zhihu.com/p/56486633

https://www.jianshu.com/p/397449cadc9a

https://blog.csdn.net/davidsguo008/article/details/73556811

Epoll是Linux进行IO多路复用的一种方式,用于在一个线程里监听多个IO源,在IO源可用的时候返回并进行操作。它的特点是基于事件驱动,性能很高。

epoll将文件描述符拷贝到内核空间后使用红黑树进行维护,同时向内核注册每个文件描述符的回调函数,当某个文件描述符可读可写的时候,将这个文件描述符加入到就绪链表里,并唤起进程,返回就绪链表到用户空间,由用户程序进行处理。

Epoll有三个系统调用:epoll_create(),epoll_ctl()和epoll_wait()。

  • eoll_create()函数在内核中初始化一个eventpoll对象,同时初始化红黑树和就绪链表。

  • epoll_ctl()用来对监听的文件描述符进行管理。将文件描述符插入红黑树,或者从红黑树中删除,这个过程的时间复杂度是log(N)。同时向内核注册文件描述符的回调函数。

  • epoll_wait()会将进程放到eventpoll的等待队列中,将进程阻塞,当某个文件描述符IO可用时,内核通过回调函数将该文件描述符放到就绪链表里,epoll_wait()会将就绪链表里的文件描述符返回到用户空间。

    (4) IO复用的三种方法(select,poll,epoll)深入理解,包括三者区别,内部原理实现?

    (1)select的方法介绍:select把所有监听的文件描述符拷贝到内核中,挂起进程。当某个文件描述符可读或可写的时候,中断程序唤起进程,select将监听的文件描述符再次拷贝到用户空间,然select后遍历这些文件描述符找到IO可用的文件。下次监控的时候需要再次拷贝这些文件描述符到内核空间。select支持监听的描述符最大数量是1024.
    select
    (2)poll使用链表保存文件描述符,其他的跟select没有什么不同。

(3)epoll将文件描述符拷贝到内核空间后使用红黑树进行维护,同时向内核注册每个文件描述符的回调函数,当某个文件描述符可读可写的时候,将这个文件描述符加入到就绪链表里,并唤起进程,返回就绪链表到用户空间。
epoll
详见 https://www.cnblogs.com/Anker/p/3265058.html

(5) Epoll的ET模式和LT模式(ET的非阻塞)

  • ET是边缘触发模式,在这种模式下,只有当描述符从未就绪变成就绪时,内核才会通过epoll进行通知。然后直到下一次变成就绪之前,不会再次重复通知。也就是说,如果一次就绪通知之后不对这个描述符进行IO操作导致它变成未就绪,内核也不会再次发送就绪通知。优点就是只通知一次,减少内核资源浪费,效率高。缺点就是不能保证数据的完整,有些数据来不及读可能就会无法取出。
  • LT是水平触发模式,在这个模式下,如果文件描述符IO就绪,内核就会进行通知,如果不对它进行IO操作,只要还有未操作的数据,内核都会一直进行通知。优点就是可以确保数据可以完整输出。缺点就是由于内核会一直通知,会不停从内核空间切换到用户空间,资源浪费严重。

(6) 查询进程占用CPU的命令(注意要了解到used,buf,代表意义)

详见:https://blog.csdn.net/qq_36357820/article/details/76606113

  1. top命令查看linux负载:
  2. uptime查看linux负载
  3. w查看linux负载:
  4. vmstat查看linux负载

    (7) linux的其他常见命令(kill,find,cp等等)

    (8) shell脚本用法

    (9) 硬连接和软连接的区别

    (10) 文件权限怎么看(rwx)

    (11) 文件的三种时间(mtime, atime,ctime),分别在什么时候会改变

    (12) Linux监控网络带宽的命令,查看特定进程的占用网络资源情况命令

    (13)Linux中线程的同步方式有哪些?

    (14)怎么修改一个文件的权限

    chmod 777 (177 277 477 等,权限组合是 1 2 4,分别代表r x w )

(15)查看文件内容常用命令

详见: http://blog.sina.com.cn/s/blog_7b4ce6b101018l8l.html

  1. cat 与 tac
    1
    2
    3
    4
    5
    6
    7
    cat的功能是将文件从第一行开始连续的将内容输出在屏幕上。当文件大,行数比较多时,屏幕无法全部容下时,只能看到一部分内容。所以通常使用重定向的方式,输出满足指定格式的内容

    cat语法:cat [-n] 文件名 (-n : 显示时,连行号一起输出)

    tac的功能是将文件从最后一行开始倒过来将内容数据输出到屏幕上。我们可以发现,tac实际上是cat反过来写。这个命令不常用。

    tac语法:tac 文件名。
  2. more和less(常用)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    more的功能是将文件从第一行开始,根据输出窗口的大小,适当的输出文件内容。当一页无法全部输出时,可以用“回车键”向下翻行,用“空格键”向下翻页。退出查看页面,请按“q”键。另外,more还可以配合管道符“|”(pipe)使用,例如:ls -al | more

    more的语法:more 文件名

    Enter 向下n行,需要定义,默认为1行;

    Ctrl f 向下滚动一屏;

    空格键 向下滚动一屏;

    Ctrl b 返回上一屏;

    = 输出当前行的行号;

    :f 输出文件名和当前行的行号;

    v 调用vi编辑器;

    ! 命令 调用Shell,并执行命令;

    q 退出more


    less的功能和more相似,但是使用more无法向前翻页,只能向后翻。

    less可以使用【pageup】和【pagedown】键进行前翻页和后翻页,这样看起来更方便。

    less的语法:less 文件名
  3. head和tail
    1
    2
    3
    4
    5
    6
    7
    head和tail通常使用在只需要读取文件的前几行或者后几行的情况下使用。head的功能是显示文件的前几行内容

    head的语法:head [n number] 文件名 (number 显示行数)

    tail的功能恰好和head相反,只显示最后几行内容

    tail的语法:tail [-n number] 文件名
  4. nl
    1
    2
    3
    nl的功能和cat -n一样,同样是从第一行输出全部内容,并且把行号显示出来

    nl的语法:nl 文件名
  5. vim

这个用的太普遍了,主要是用于编辑。

(16)怎么找出含有关键字的前后4行

(17)Linux的GDB调试

(18)coredump是什么 怎么才能coredump

coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

coredump产生的条件

  1. shell资源控制限制,使用 ulimit -c 命令查看shell执行程序时的资源 ,如果为0,则不会产生coredump。可以用ulimit -c unlimited设置为不限大小。
  2. 读写越界,包括:数组访问越界,指针指向错误的内存,字符串读写越界
  3. 使用了线程不安全的函数,读写未加锁保护
  4. 错误使用指针转换
  5. 堆栈溢出

    (19)tcpdump常用命令

    用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具。 tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。

实用命令实例

将某端口收发的数据包保存到文件

sudo tcpdump -i any port 端口 -w 文件名.cap

打印请求到屏幕

sudo tcpdump -i any port 端口 -Xnlps0

默认启动

tcpdump
普通情况下,直接启动tcpdump将监视第一个网络接口上所有流过的数据包。
监视指定网络接口的数据包

tcpdump -i eth1
如果不指定网卡,默认tcpdump只会监视第一个网络接口,一般是eth0,下面的例子都没有指定网络接口。 

(20) crontab命令

详见:https://www.cnblogs.com/peida/archive/2013/01/08/2850483.html

corntab命令是用来指定用户计划任务的。用户将需要定时执行的任务写入crontab文件中,提交给crond进程定期执行。

  • crontab命令用来对crontab文件进行管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    1.命令格式:
    crontab [-u user] file
    crontab [-u user] [ -e | -l | -r ]
    2.命令功能:
    通过crontab 命令,我们可以在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。这个命令非常设合周期性的日志分析或数据备份等工作。
    3.命令参数:
    -u user:用来设定某个用户的crontab服务,例如,“-u ixdba”表示设定ixdba用户的crontab服务,此参数一般有root用户来运行。
    file:file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。如果在命令行中没有指定这个文件,crontab命令将接受标准输入(键盘)上键入的命令,并将它们载入crontab。
    -e:编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。
    -l:显示某个用户的crontab文件内容,如果不指定用户,则表示显示当前用户的crontab文件内容。
    -r:从/var/spool/cron目录中删除某个用户的crontab文件,如果不指定用户,则默认删除当前用户的crontab文件。
    -i:在删除用户的crontab文件时给确认提示。
  • crontab文件内容

crond是Linux下的周期性执行系统任务的守护进程,他会根据/etc下的crontab配置文件的内容执行。用户需要将计划任务写入crontab文件中才能执行。

用户所建立的crontab文件中,每一行都代表一项任务,每行的每个字段代表一项设置,它的格式共分为六个字段,前五段是时间设定段,第六段是要执行的命令段,格式如下:

1
minute   hour   day   month   week   command

其中:

  • minute: 表示分钟,可以是从0到59之间的任何整数。
  • hour:表示小时,可以是从0到23之间的任何整数。
  • day:表示日期,可以是从1到31之间的任何整数。
  • month:表示月份,可以是从1到12之间的任何整数。
  • week:表示星期几,可以是从0到7之间的任何整数,这里的0或7代表星期日。
  • command:要执行的命令,可以是系统命令,也可以是自己编写的脚本文件。

在以上各个字段中,还可以使用以下特殊字符:

  • 星号(*):代表所有可能的值,例如month字段如果是星号,则表示在满足其它字段的制约条件后每月都执行该命令操作。
  • 逗号(,):可以用逗号隔开的值指定一个列表范围,例如,“1,2,5,7,8,9”
  • 中杠(-):可以用整数之间的中杠表示一个整数范围,例如“2-6”表示“2,3,4,5,6”
  • 正斜线(/):可以用正斜线指定时间的间隔频率,例如“0-23/2”表示每两小时执行一次。同时正斜线可以和星号一起使用,例如*/10,如果用在minute字段,表示每十分钟执行一次。

(21) 查看后台进程

  • jobs

查看当前控制台的后台进程

想要停止后台进程,使用jobs命令查看其进程号(比如为num),然后kill %num即可

  • ps

查看后台进程

  • top

查看所有进程和资源使用情况,类似Windows中的任务管理器

停止进程:界面是交互式的,在窗口输入k 之后输入PID,会提示输入停止进程模式 有SIGTERM和 SIGKILL 如果留空不输入,就是SIGTERM(优雅停止)

退出top:输入q即可

5. 操作系统

(1) 进程与线程的区别和联系(重点)

  • 区别
  1. 进程是对运行时程序的封装,是系统进行资源分配和调度的基本单元,而线程是进程的子任务,是CPU分配和调度的基本单元。
  2. 一个进程可以有多个线程,但是一个线程只能属于一个进程。
  3. 进程的创建需要系统分配内存和CPU,文件句柄等资源,销毁时也要进行相应的回收,所以进程的管理开销很大;但是线程的管理开销则很小。
  4. 进程之间不会相互影响;而一个线程崩溃会导致进程崩溃,从而影响同个进程里面的其他线程。
  • 联系 进程与线程之间的关系:线程是存在进程的内部,一个进程中可以有多个线程,一个线程只能存在一个进程中。

    (2) Linux理论上最多可以创建多少个进程?一个进程可以创建多少线程,和什么有关

    答:32768. 因为进程的pid是用pid_t来表示的,pid_t的最大值是32768.所以理论上最多有32768个进程。

至于线程。进程最多可以创建的线程数是根据分配给调用栈的大小,以及操作系统(32位和64位不同)共同决定的。Linux32位下是300多个。

(3) 冯诺依曼结构有哪几个模块?分别对应现代计算机的哪几个部分?(百度安全一面)

  • 存储器:内存
  • 控制器:南桥北桥
  • 运算器:CPU
  • 输入设备:键盘
  • 输出设备:显示器、网卡

    (4) 进程之间的通信方法有哪几种 (重点)

    进程之间的通信方式主要有六种,包括管道,信号量,消息队列,信号,共享内存,套接字

  • 管道:管道是半双工的,双方需要通信的时候,需要建立两个管道。管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。管道是最容易实现的
    fig/管道通信.png

    匿名管道pipe和命名管道除了建立,打开,删除的方式不同外,其余都是一样的。匿名管道只允许有亲缘关系的进程之间通信,也就是父子进程之间的通信,命名管道允许具有非亲缘关系的进程间通信。

    管道的底层实现 https://segmentfault.com/a/1190000009528245

  • 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量只有等待和发送两种操作。等待(P(sv))就是将其值减一或者挂起进程,发送(V(sv))就是将其值加一或者将进程恢复运行。

  • 信号:信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 信号是开销最小的

  • 共享内存:共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,就像由malloc()分配的内存一样使用。一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。共享内存的效率最高,缺点是没有提供同步机制,需要使用锁等其他机制进行同步。

  • 消息队列:消息队列就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
    消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
    可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

  • 套接字:套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。

    (5) 进程调度方法详细介绍

    https://blog.csdn.net/u011080472/article/details/51217754

https://blog.csdn.net/leex_brave/article/details/51638300

  • 先来先服务 (FCFS first come first serve):按照作业到达任务队列的顺序调度 FCFS是非抢占式的,易于实现,效率不高,性能不好,有利于长作业(CPU繁忙性)而不利于短作业(I/O繁忙性)。
  • 短作业优先 (SHF short job first):每次从队列里选择预计时间最短的作业运行。SJF是非抢占式的,优先照顾短作业,具有很好的性能,降低平均等待时间,提高吞吐量。但是不利于长作业,长作业可能一直处于等待状态,出现饥饿现象;完全未考虑作业的优先紧迫程度,不能用于实时系统。
  • 最短剩余时间优先 该算法首先按照作业的服务时间挑选最短的作业运行,在该作业运行期间,一旦有新作业到达系统,并且该新作业的服务时间比当前运行作业的剩余服务时间短,则发生抢占;否则,当前作业继续运行。该算法确保一旦新的短作业或短进程进入系统,能够很快得到处理。
  • 高响应比优先调度算法(Highest Reponse Ratio First, HRRF)是非抢占式的,主要用于作业调度。基本思想:每次进行作业调度时,先计算后备作业队列中每个作业的响应比,挑选最高的作业投入系统运行。响应比 = (等待时间 + 服务时间) / 服务时间 = 等待时间 / 服务时间 + 1。因为每次都需要计算响应比,所以比较耗费系统资源。
  • 时间片轮转 用于分时系统的进程调度。基本思想:系统将CPU处理时间划分为若干个时间片(q),进程按照到达先后顺序排列。每次调度选择队首的进程,执行完1个时间片q后,计时器发出时钟中断请求,该进程移至队尾。以后每次调度都是如此。该算法能在给定的时间内响应所有用户的而请求,达到分时系统的目的。
  • 多级反馈队列(Multilevel Feedback Queue)

    (6) 进程的执行过程是什么样的,执行一个进程需要做哪些工作?

    进程的执行需要经过三大步骤:编译,链接和装入。
  • 编译:将源代码编译成若干模块
  • 链接:将编译后的模块和所需要的库函数进行链接。链接包括三种形式:静态链接,装入时动态链接(将编译后的模块在链接时一边链接一边装入),运行时动态链接(在执行时才把需要的模块进行链接)
  • 装入:将模块装入内存运行

https://blog.csdn.net/qq_38623623/article/details/78306498

将进程装入内存时,通常使用分页技术,将内存分成固定大小的页,进程分为固定大小的块,加载时将进程的块装入页中,并使用页表记录。减少外部碎片。

通常操作系统还会使用虚拟内存的技术将磁盘作为内存的扩充。

(6) 操作系统的内存管理说一下

https://www.cnblogs.com/peterYong/p/6556619.html

https://zhuanlan.zhihu.com/p/141602175

操作系统的内存管理包括物理内存管理和虚拟内存管理

  • 物理内存管理包括交换与覆盖,分页管理,分段管理和段页式管理等;
  • 虚拟内存管理包括虚拟内存的概念,页面置换算法,页面分配策略等;

(面试官这样问的时候,其实是希望你能讲讲虚拟内存)

(7) 实现一个LRU算法

用到两个数据结构:哈希+双向链表

1
2
unordered_map<int,list<pair<int,int> > > cache ;// 存放键,迭代器
list<pair<int,int>> auxlist; // 存放 <键,值>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class LRUCache {
int cap;
list<pair<int,int>> l;// front:new back:old 存放值 新的放前面,因为前面的可以取得有效的迭代器
map<int,list<pair<int,int> >::iterator > cache;// 存放键,迭代器
public:
LRUCache(int capacity) {
cap=capacity;
}

int get(int key) {
auto mapitera = cache.find(key);
if(mapitera==cache.end()){
return -1;
}else{// found
list<pair<int,int>>::iterator listItera = mapitera->second;
int value = (*listItera).second;

l.erase(listItera);
l.push_front({key,value});
cache[key]=l.begin();

return value;
}
}

void put(int key, int value) {
auto itera = cache.find(key);
if(itera!=cache.end()){// exist
list<pair<int,int>>::iterator listItera = itera->second;

l.erase(listItera);
l.push_front({key,value});
cache[key]=l.begin();

}else{// not exist
if(cache.size()>=cap){
pair<int,int> oldpair = l.back();
l.pop_back();
cache.erase(oldpair.first);
}
l.push_front({key,value});
cache[key]=l.begin();
}
}
};

/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/

(8) 死锁产生的必要条件(怎么检测死锁,解决死锁问题)

(1) 互斥:一个资源每次只能被一个进程使用。

(2) 占有并请求:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

产生死锁的原因主要是:

(1) 因为系统资源不足。

(2) 进程运行推进的顺序不合适。

(3) 资源分配不当等。

(8) 死锁的恢复

  1. 重新启动:是最简单、最常用的死锁消除方法,但代价很大,因为在此之前所有进程已经完成的计算工作都将付之东流,不仅包括死锁的全部进程,也包括未参与死锁的全部进程。
  2. 终止进程(process termination):终止参与死锁的进程并回收它们所占资源。
    (1) 一次性全部终止;(2) 逐步终止(优先级,代价函数)
  3. 剥夺资源(resource preemption):剥夺死锁进程所占有的全部或者部分资源。
    (1) 逐步剥夺:一次剥夺死锁进程所占有的一个或一组资源,如果死锁尚未解除再继续剥夺,直至死锁解除为止。
    (2) 一次剥夺:一次性地剥夺死锁进程所占有的全部资源。
  4. 进程回退(rollback):让参与死锁的进程回退到以前没有发生死锁的某个点处,并由此点开始继续执行,希望进程交叉执行时不再发生死锁。但是系统开销很大:
    (1) 要实现“回退”,必须“记住”以前某一点处的现场,而现场随着进程推进而动态变化,需要花费大量时间和空间。
    (2) 一个回退的进程应当“挽回”它在回退点之间所造成的影响,如修改某一文件,给其它进程发送消息等,这些在实现时是难以做到的

(8)什么是饥饿

饥饿是由于资源分配策略不公引起的,当进程或线程无法访问它所需要的资源而不能继续执行时,就会发生饥饿现象。

(9) 如果要你实现一个mutex互斥锁你要怎么实现?

https://blog.csdn.net/kid551/article/details/84338619

实现mutex最重要的就是实现它的lock()方法和unlock()方法。我们保存一个全局变量flag,flag=1表明该锁已经锁住,flag=0表明锁没有锁住。
实现lock()时,使用一个while循环不断检测flag是否等于1,如果等于1就一直循环。然后将flag设置为1;unlock()方法就将flag置为0;

1
2
3
4
5
6
7
8
9
static int flag=0;

void lock(){
while(TestAndSet(&flag,1)==1);
//flag=1;
}
void unlock(){
flag=0;
}

因为while有可能被重入,所以可以用TestandSet()方法。
1
2
3
4
5
int TestAndSet(int *ptr, int new) {
int old = *ptr;
*ptr = new;
return old;
}

(10)线程之间的通信方式有哪些? 进程之间的同步方式又哪些?

线程之间通信:

  • 使用全局变量
  • 使用信号机制
  • 使用事件

进程之间同步:
https://www.cnblogs.com/sonic4x/archive/2011/07/05/2098036.html

  • 信号量
  • 管程

    (13) 什么时候用多进程,什么时候用多线程

    https://blog.csdn.net/yu876876/article/details/82810178

  • 频繁修改:需要频繁创建和销毁的优先使用多线程

  • 计算量:需要大量计算的优先使用多线程 因为需要消耗大量CPU资源且切换频繁,所以多线程好一点
  • 相关性:任务间相关性比较强的用多线程,相关性比较弱的用多进程。因为线程之间的数据共享和同步比较简单。
  • 多分布:可能要扩展到多机分布的用多进程,多核分布的用多线程

但是实际中更常见的是进程加线程的结合方式,并不是非此即彼的。

(14) 文件读写使用的系统调用

(15) 孤儿进程和僵尸进程分别是什么,怎么形成的?

https://www.cnblogs.com/Anker/p/3271773.html

  • 孤儿进程是父进程退出后它的子进程还在执行,这时候这些子进程就成为孤儿进程。孤儿进程会被init进程收养并完成状态收集。
  • 僵尸进程是指子进程完成并退出后父进程没有使用wait()或者waitpid()对它们进行状态收集,这些子进程的进程描述符仍然会留在系统中。这些子进程就成为僵尸进程。

    (16) 说一下PCB/说一下进程地址空间/

    https://blog.csdn.net/qq_38499859/article/details/80057427

PCB就是进程控制块,是操作系统中的一种数据结构,用于表示进程状态,操作系统通过PCB对进程进行管理。

PCB中包含有:进程标识符,处理器状态,进程调度信息,进程控制信息

进程地址空间内有:

  • 代码段text:存放程序的二进制代码
  • 初始化的数据Data:已经初始化的变量和数据
  • 未初始化的数据BSS:还没有初始化的数据
  • (17) 内核空间和用户空间是怎样区分的

    在Linux中虚拟地址空间范围为0到4G,最高的1G地址(0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间,低的3G空间(0x00000000到0xBFFFFFFF)供各个进程使用,就是用户空间。

内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。

(18) 多线程是如何同步的(尤其是如果项目中用到了多线程,很大可能会结合讨论)

https://blog.csdn.net/s_lisheng/article/details/74278765

  • 临界区
  • 信号量
  • 事件
  • 互斥量

    (19) 同一个进程内的线程会共享什么资源?

  • 该进程的地址空间
  • 全局变量
  • 堆空间

线程的栈空间是自己独有的

(20) 异常和中断的区别

(21) 一般情况下在Linux/windows平台下栈空间的大小

在Linux下栈空间通常是8M,Windows下是1M

(22)虚拟内存的了解

https://www.cnblogs.com/Przz/p/6876988.html

在运行一个进程的时候,它所需要的内存空间可能大于系统的物理内存容量。通常一个进程会有4G的空间,但是物理内存并没有这么大,所以这些空间都是虚拟内存,它的地址都是逻辑地址,每次在访问的时候都需要映射成物理地址。
当进程访问某个逻辑地址的时候,会去查看页表,如果页表中没有相应的物理地址,说明内存中没有这页的数据,发生缺页异常,这时候进程需要把数据从磁盘拷贝到物理内存中。如果物理内存已经满了,就需要覆盖已有的页,如果这个页曾经被修改过,那么还要把它写回磁盘。

(23)服务器高并发的解决方案

  1. 应用数据与静态资源分离
    将静态资源(图片,视频,js,css等)单独保存到专门的静态资源服务器中,在客户端访问的时候从静态资源服务器中返回静态资源,从主服务器中返回应用数据。

  2. 客户端缓存
    因为效率最高,消耗资源最小的就是纯静态的html页面,所以可以把网站上的页面尽可能用静态的来实现,在页面过期或者有数据更新之后再将页面重新缓存。或者先生成静态页面,然后用ajax异步请求获取动态数据。

  3. 集群和分布式
    (集群是所有的服务器都有相同的功能,请求哪台都可以,主要起分流作用)

    (分布式是将不同的业务放到不同的服务器中,处理一个请求可能需要使用到多台服务器,起到加快请求处理的速度。)

    可以使用服务器集群和分布式架构,使得原本属于一个服务器的计算压力分散到多个服务器上。同时加快请求处理的速度。

  4. 反向代理
    在访问服务器的时候,服务器通过别的服务器获取资源或结果返回给客户端。

    (24)协程了解吗(高频)

    协程和微线程是一个东西。

协程就是子程序在执行时中断并转去执行别的子程序,在适当的时候又返回来执行。
这种子程序间的跳转不是函数调用,也不是多线程执行,所以省去了线程切换的开销,效率很高,并且不需要多线程间的锁机制,不会发生变量写冲突。

(25)那协程的底层是怎么实现的,怎么使用协程?

协程进行中断跳转时将函数的上下文存放在其他位置中,而不是存放在函数堆栈里,当处理完其他事情跳转回来的时候,取回上下文继续执行原来的函数。

(23)进程的状态以及转换图

  • 三态模型
    三态模型包括三种状态:
    1. 执行:进程分到CPU时间片,可以执行
    2. 就绪:进程已经就绪,只要分配到CPU时间片,随时可以执行
    3. 阻塞:有IO事件或者等待其他资源
  • 五态模型

    1. 新建态:进程刚刚创建。
    2. 就绪态:
    3. 运行态:
    4. 等待态:出现等待事件
    5. 终止态:进程结束
  • 七态模型

    1. 新建态
    2. 就绪挂起态
    3. 就绪态
    4. 运行态
    5. 等待态
    6. 挂起等待态
    7. 终止态

(24)在执行malloc申请内存的时候,操作系统是怎么做的?/内存分配的原理说一下/malloc函数底层是怎么实现的?/进程是怎么分配内存的?

https://blog.csdn.net/yusiguyuan/article/details/39496057

从操作系统层面上看,malloc是通过两个系统调用来实现的: brk和mmap

  • brk是将进程数据段(.data)的最高地址指针向高处移动,这一步可以扩大进程在运行时的堆大小
  • mmap是在进程的虚拟地址空间中寻找一块空闲的虚拟内存,这一步可以获得一块可以操作的堆内存。

通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。

进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。

(25)什么是字节序?怎么判断是大端还是小端?有什么用?

https://www.cnblogs.com/broglie/p/5645200.html

字节序是对象在内存中存储的方式,大端即为最高有效位在前面,小端即为最低有效位在前面。
判断大小端的方法:使用一个union数据结构

1
2
3
4
5
6
7
union{
short s;
char c[2]; // sizeof(short)=2;
}un;
un.s=0x0102;
if(un.c[0]==1 and un.c[1]==2) cout<<"大端";
if(un.c[0]==2 and un.c[1]==1) cout<<"小端";

在网络编程中不同字节序的机器发送和接收的顺序不同。

6. 场景题/算法题

(0) leetcode hot100至少刷两遍,剑指offer至少刷两遍 重中之重!!

面试中90%的算法题都从leetcode hot100和剑指offer中出 刷两遍非常有必要

(1) 介绍熟悉的设计模式(单例,简单工厂模式)

(2) 写单例模式,线程安全版本

version
1
2
3
4
5
6
7
8
9
10
11
12
class Singleton{
private:
static Singleton* instance;
Singleton(){
// initialize
}
public:
static Singleton* getInstance(){
if(instance==nullptr) instance=new Singleton();
return instance;
}
};

(3) 写三个线程交替打印ABC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;

mutex mymutex;
condition_variable cv;
int flag=0;

void printa(){
unique_lock<mutex> lk(mymutex);
int count=0;
while(count<10){
while(flag!=0) cv.wait(lk);
cout<<"thread 1: a"<<endl;
flag=1;
cv.notify_all();
count++;
}
cout<<"my thread 1 finish"<<endl;
}
void printb(){
unique_lock<mutex> lk(mymutex);
for(int i=0;i<10;i++){
while(flag!=1) cv.wait(lk);
cout<<"thread 2: b"<<endl;
flag=2;
cv.notify_all();
}
cout<<"my thread 2 finish"<<endl;
}
void printc(){
unique_lock<mutex> lk(mymutex);
for(int i=0;i<10;i++){
while(flag!=2) cv.wait(lk);
cout<<"thread 3: c"<<endl;
flag=0;
cv.notify_all();
}
cout<<"my thread 3 finish"<<endl;
}
int main(){
thread th2(printa);
thread th1(printb);
thread th3(printc);

th1.join();
th2.join();
th3.join();
cout<<" main thread "<<endl;


}

(4) 二维码登录的实现过程 场景题

(5) 不使用临时变量实现swap函数

  • 使用异或/加减等方式,下面给出使用异或的实现方法
    1
    2
    3
    4
    5
    void swap(int& a,int& b){
    a=a^b;
    b=a^b;
    a=a^b;
    }

    (6) 实现一个strcpy函数(或者memcpy),如果内存可能重叠呢

    (7) 实现快排

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void swap(vector<int>& vec,int a,int b){
    vec[a]=vec[a]^vec[b];
    vec[b]=vec[a]^vec[b];
    vec[a]=vec[a]^vec[b];
    }
    int partition(vector<int>& vec,int start,int end){
    int pivot=vec[start+(end-start)/2];
    while(start<end){
    while(start<end and vec[start]<pivot) start++;
    while(start<end and vec[end]>pivot) end--;
    if(start<end) swap(vec,start,end);
    }
    return start;
    }
    void quickSort(vector<int>& vec,int start,int end){
    if(start>end) return;
    int pivot=partition(vec,start,end);
    quickSort(vec,start,pivot-1);
    quickSort(vec,pivot+1,end);
    }

    (8) 实现一个堆排序

    堆排序的基本过程:
  • 将n个元素的序列构建一个大顶堆或小顶堆
  • 将堆顶的元素放到序列末尾
  • 将前n-1个元素重新构建大顶堆或小顶堆,重复这个过程,直到所有元素都已经排序

整体时间复杂度为nlogn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<iostream>
#include<vector>
using namespace std;
void swap(vector<int>& arr, int a,int b){
arr[a]=arr[a]^arr[b];
arr[b]=arr[a]^arr[b];
arr[a]=arr[a]^arr[b];
}
void adjust(vector<int>& arr,int len,int index){
int maxid=index;
// 计算左右子节点的下标 left=2*i+1 right=2*i+2 parent=(i-1)/2
int left=2*index+1,right=2*index+2;

// 寻找当前以index为根的子树中最大/最小的元素的下标
if(left<len and arr[left]<arr[maxid]) maxid=left;
if(right<len and arr[right]<arr[maxid]) maxid=right;

// 进行交换,记得要递归进行adjust,传入的index是maxid
if(maxid!=index){
swap(arr,maxid,index);
adjust(arr,len,maxid);
}
}
void heapsort(vector<int>&arr,int len){
// 初次构建堆,i要从最后一个非叶子节点开始,所以是(len-1-1)/2,0这个位置要加等号
for(int i=(len-1-1)/2;i>=0;i--){
adjust(arr,len,i);
}

// 从最后一个元素的下标开始往前遍历,每次将堆顶元素交换至当前位置,并且缩小长度(i为长度),从0处开始adjust
for(int i=len-1;i>0;i--){
swap(arr,0,i);
adjust(arr,i,0);// 注意每次adjust是从根往下调整,所以这里index是0!
}
}
int main(){
vector<int> arr={3,4,2,1,5,8,7,6};

cout<<"before: "<<endl;
for(int item:arr) cout<<item<<" ";
cout<<endl;

heapsort(arr,arr.size());

cout<<"after: "<<endl;
for(int item:arr)cout<<item<<" ";
cout<<endl;

return 0;
}

(8) 实现一个插入排序

https://blog.csdn.net/left_la/article/details/8656425

1
2
3
4
5
6
7
8
9
10
11
12
void insertSort(vector<int>& nums){
int len=nums.size();
for(int i=1;i<len;i++){
int key=nums[i];
int j=i-1;
while(j>=0 and nums[j]>key){
nums[j+1]=nums[j];
j--;
}
nums[j+1]=key;
}
}

(9) 快排存在的问题,如何优化

  • 3 种快排基准选择方法:

随机(rand函数)、固定(队首、队尾)、三数取中(队首、队中和队尾的中间数)

  • 4种优化方式:

优化1:当待排序序列的长度分割到一定大小后,使用插入排序

优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割

优化3:优化递归操作

优化4:使用并行或多线程处理子序列

(10) 反转一个链表(招银网络二面)

1
2
3
4
5
6
7
8
9
ListNode* reverse(ListNode* root){
ListNode* pre=nullptr,cur=root,nxt;
while(cur!=nullptr){
nxt=cur->next;
cur->next=pre;
pre=cur;cur=nxt;
}
return pre;
}

(11) Top K问题(可以采取的方法有哪些,各自优点?)(重点)

Top K 问题的常见形式:

给定10000个整数,找第K大(第K小)的数

给定10000个整数,找出最大(最小)的前K个数

给定100000个单词,求前K词频的单词

解决Top K问题若干种方法

  • 使用最大最小堆。求最大的数用最小堆,求最小的数用最大堆。
  • Quick Select算法。使用类似快排的思路,根据pivot划分数组。
  • 使用排序方法,排序后再寻找top K元素。
  • 使用选择排序的思想,对前K个元素部分排序。
  • 将1000…..个数分成m组,每组寻找top K个数,得到m×K个数,在这m×k个数里面找top K个数。
  1. 使用最大最小堆的思路 (以top K 最大元素为例)

    按顺序扫描这10000个数,先取出K个元素构建一个大小为K的最小堆。每扫描到一个元素,如果这个元素大于堆顶的元素(这个堆最小的一个数),就放入堆中,并删除堆顶的元素,同时整理堆。如果这个元素小于堆顶的元素,就直接pass。最后堆中剩下的元素就是最大的前Top K个元素,最右的叶节点就是Top 第K大的元素。

note:最小堆的插入时间复杂度为log(n),n为堆中元素个数,在这里是K。最小堆的初始化时间复杂度是nlog(n)

C++中的最大最小堆要用标准库的priority_queue来实现。

1
2
3
4
5
6
7
8
9
10
11
12
struct Node {
int value;
int idx;
Node (int v, int i): value(v), idx(i) {}
friend bool operator < (const struct Node &n1, const struct Node &n2) ;
};

inline bool operator < (const struct Node &n1, const struct Node &n2) {
return n1.value < n2.value;
}

priority_queue<Node> pq; // 此时pq为最大堆

  1. 使用Quick Select的思路(以寻找第K大的元素为例)

    Quick Select脱胎于快速排序,提出这两个算法的都是同一个人。算法的过程是这样的:
    首先选取一个枢轴,然后将数组中小于该枢轴的数放到左边,大于该枢轴的数放到右边。
    此时,如果左边的数组中的元素个数大于等于K,则第K大的数肯定在左边数组中,继续对左边数组执行相同操作;
    如果左边的数组元素个数等于K-1,则第K大的数就是pivot;
    如果左边的数组元素个数小于K,则第K大的数肯定在右边数组中,对右边数组执行相同操作。

这个算法与快排最大的区别是,每次划分后只处理左半边或者右半边,而快排在划分后对左右半边都继续排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//此为Java实现
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, k, 0, nums.length - 1);
}

// quick select to find the kth-largest element
public int quickSelect(int[] arr, int k, int left, int right) {
if (left == right) return arr[right];
int index = partition(arr, left, right);
if (index - left + 1 > k)
return quickSelect(arr, k, left, index - 1);
else if (index - left + 1 == k)
return arr[index];
else
return quickSelect(arr, k - (index - left + 1), index + 1, right);

}

  1. 使用选择排序的思想对前K个元素排序 ( 以寻找前K大个元素为例)

    扫描一遍数组,选出最大的一个元素,然后再扫描一遍数组,找出第二大的元素,再扫描一遍数组,找出第三大的元素。。。。。以此类推,找K个元素,时间复杂度为O(N*K)

    (12) 8G的int型数据,计算机的内存只有2G,怎么对它进行排序?(外部排序)(百度一面)

    我们可以使用外部排序来对它进行处理。首先将整个文件分成许多份,比如说m份,划分的依据就是使得每一份的大小都能放到内存里。然后我们用快速排序或者堆排序等方法对每一份数据进行一个内部排序,变成有序子串。接着对这m份有序子串进行m路归并排序。取这m份数据的最小元素,进行排序,输出排序后最小的元素到结果中,同时从该元素所在子串中读入一个元素,直到所有数据都被输出到结果中为止。

https://blog.csdn.net/ailunlee/article/details/84548950

(13) 自己构建一棵二叉树,使用带有null标记的前序遍历序列

在写二叉树相关算法的时候,如果需要自己构造测试用例(自己构造一棵二叉树),往往是一件很麻烦的事情,我们可以用一个带有null标记的前序遍历序列来进行构造。 需要注意的是vec2tree()参数中的start是引用传递,而不是简单的参数值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
#include<vector>
#include<queue>
using namespace std;

struct treeNode{
string val;
treeNode* left,*right;
treeNode(string val):val(val){
left=nullptr;
right=nullptr;
}
};

treeNode* vec2tree(vector<string>& vec,int& start){
treeNode* root;
if(vec[start]=="null"){
start+=1;
root=nullptr;
}else{
root=new treeNode(vec[start]);
start+=1;
root->left=vec2tree(vec,start);
root->right=vec2tree(vec,start);
}
return root;
}

void tree2vec(treeNode *root,vector<string>& vec){
if(root==nullptr){
vec.push_back("null");
}else{
vec.push_back(root->val);
tree2vec(root->left,vec);
tree2vec(root->right,vec);
}
}

int main(){
vector<string> vec={"2","4","5","7","null","null","null","null","3","6","null","null","2","null","null"};
int index=0,&start=index;
treeNode* root=vec2tree(vec,start);
//displaytree(root);
vector<string> mvec;
tree2vec(root,mvec);
for(string item:mvec) cout<<item<<" ";
cout<<endl;
return 0;

(14) 介绍一下b树和它的应用场景有哪些

B树也叫做B-树,或者平衡多路树,它是每个节点最多有m个子树的平衡树。一个m阶的B树具有如下几个特征:

  1. 根结点至少有两个子女。
  2. 每个中间节点都包含至多m个子树 , 每个节点包含的元素个数是其子树个数-1(其中 m/2 <= k <= m)
  3. 所有的叶子结点都位于同一层。
  4. 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个子树包含的元素的值域分划。

b树主要应用于文件系统中,在数据库中(mongoDB)也有应用,与B+树相比好处应该是有时不需要访问到叶节点就可以获取数据。

查询时间复杂度是logN

(15) 介绍一下b+树和它的应用场景有哪些

B+树是一种特殊的B树,它把数据都存储在叶子节点,并且叶节点间有指针连接。内部只存关键字(其中叶子节点的最小值作为索引)和孩子指针,简化了内部节点。

应用场景主要是数据库的索引

查询时间复杂度也是logN
https://zhuanlan.zhihu.com/p/110202102

https://blog.csdn.net/hguisu/article/details/7786014

(16) 介绍一下红黑树和它的应用场景有哪些

红黑树是一种特殊的二叉查找树,它在每一个节点上都使用红色或黑色进行标记,通过一些性质确保它是始终平衡的。
它的性质是这样的:

  1. 每个节点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 叶节点的空节点是黑色的。
  4. 如果一个节点是红色的,那么它的两个子节点是黑色的。
  5. 对于任意节点,从它到叶节点的每条路径上都有相同数目的黑色节点。

红黑树的插入,查询,删除在一般情况和最坏情况下的时间复杂度都是O(log(n))

应用场景主要是STL中map,set的实现,优点在于支持频繁的修改,因为查询删除插入时间复杂度都是logN

(17) 怎么写sql取表的前1000行数据(招银网络二面)

1
2
select * limit 1000
from t1

(18) N个骰子出现和为m的概率

(19) 海量数据问题(可参考左神的书)

(20) 一致性哈希

(21)希尔排序说一下/手撕

https://www.cnblogs.com/chengxiao/p/6104371.html
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

(22)Dijkstra算法说一下

(23)实现一个动态数组要怎么实现,说思路(腾讯teg一面)

模拟STL中vector的实现即可,去看一下vector的源码。

(24)最小生成树算法说一下

(25) 海量数据的bitmap使用原理

bitmap算法就是使用一个比特映射一个值,它可以用在整数排序和数据压缩上,因为使用一个比特位去存储一个数,所以它可以大大节省空间。

它的具体过程是:先根据数组中元素最大的数N计算需要分配多大的空间。
如果使用int型数组的形式来保存的话,一个int = 4字节 =4*8比特 = 32比特。也就是一个int数可以映射32个数据(图1),然后需要找到最大的数Max,表示最多需要的位数,所以需要开辟的数组空间为int a[1+Max/32]。
然后需要推导一个整数a内如何映射32个数据,方法是将待存储的数据模32,然后将a中相应位置的比特置为1。
依此方法映射每一个元素,待读取的时候扫描每个比特位,遇到值为1的就还原该数字。

移位计算公式:
N/32就是将N的二进制右移log32(也就是5)位 : N>>5

N%32就是求N的后5位:N& 0x1F (0x1F = 00011111)

模32然后相应位置置为1: a[i] |= 1<< N & 0x1F

所以总的公式为: a[ N>>5 ] |= 1<< N & 0x1F

BitMap算法评价

  • 优点:
    1. 运算效率高,不进行比较和移位;
    2. 占用内存少,比如最大的数MAX=10000000;只需占用内存为MAX/8=1250000Byte=1.25M。
  • 缺点:
    1. 所有的数据不能重复,即不可对重复的数据进行排序。(少量重复数据查找还是可以的,用2-bitmap)。
    2. 所需要的空间随着最大元素的增大而增大,当数据类似(1,1000,10万)只有3个数据的时候,用bitmap时间复杂度和空间复杂度相当大,只有当数据比较密集时才有优势。

(26) 布隆过滤器原理与优点

布隆过滤器是一个比特向量或者比特数组,它本质上是一种概率型数据结构,用来查找一个元素是否在集合中,支持高效插入和查询某条记录。常作为针对超大数据量下高效查找数据的一种方法。

它的具体工作过程是这样子的:
假设布隆过滤器的大小为m(比特向量的长度为m),有k个哈希函数,它对每个数据用这k个哈希函数计算哈希,得到k个哈希值,然后将向量中相应的位设为1。在查询某个数据是否存在的时候,对这个数据用k个哈希函数得到k个哈希值,再在比特向量中相应的位查找是否为1,如果某一个相应的位不为1,那这个数据就肯定不存在。但是如果全找到了,则这个数据有可能存在。

为什么说有可能存在呢?
因为不同的数据经过哈希后可能有相同的哈希值,在比特向量上某个位置查找到1也可能是由于某个另外的数据映射得到的。

支持删除操作吗
目前布隆过滤器只支持插入和查找操作,不支持删除操作,如果要支持删除,就要另外使用一个计数变量,每次将相应的位置为1则计数加一,删除则减一。

布隆过滤器中哈希函数的个数需要选择。如果太多则很快所有位都置为1,如果太少会容易误报。

布隆过滤器的大小以及哈希函数的个数怎么选择?
k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

(27) 布隆过滤器处理大规模问题时的持久化,包括内存大小受限、磁盘换入换出问题

(28)实现一个队列,并且使它支持多线程,队列有什么应用场景(阿里三面)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//评测题目: 
class FIFOQueue
{
vector<int> vec(initCap,0);
int start=0,end=0;
condition_variable cv;
mutex m;
bool flag=false;// isFull
bool enqueue(int v) {
unique_lock<mutex></mutex> lk(m);
while(flag==true) cv.wait(lk);
end=(end+1)%initCap;
vec[end]=v;
cv.notifyall();
return true;
}
}
int dequeue() {
unique_lock<mutex></mutex> lk(m);
if(start!=end){
int val = vec[start];
start=(start+1)%initCap;
flag=false;
cv.notifyall();
return val;
}else{
flag=false;
cv.notifyall();
return -1;
}
}
}

以上代码是面试时写的,并没有运行,也许有错误,请客观参考

在main执行之前和之后执行的代码可能是什么?

main函数执行之前,主要就是初始化系统相关资源:

  • 设置栈指针
  • 初始化静态static变量和global全局变量,即.data段的内容
  • 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容
  • 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码
  • 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数

main函数执行之后:

  • 全局对象的析构函数会在main函数之后执行;
  • 可以用 atexit 注册一个函数,它会在main 之后执行;

结构体内存对齐问题?

结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。

未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

指针和引用的区别

  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。
  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void test(int *p)
{
  int a=1;
  p=&a;
  cout<<p<<" "<<*p<<endl;
}

int main(void)
{
int *p=NULL;
test(p);
if(p==NULL)
cout<<"指针p为NULL"<<endl;
return 0;
}
//运行结果为:
//0x22ff44 1
//指针p为NULL

void testPTR(int* p) {
int a = 12;
p = &a;

}

void testREFF(int& p) {
int a = 12;
p = a;

}
void main()
{
int a = 10;
int* b = &a;
testPTR(b);//改变指针指向,但是没改变指针的所指的内容
cout << a << endl;// 10
cout << *b << endl;// 10

a = 10;
testREFF(a);
cout << a << endl;//12
}

堆和栈的区别

  • 申请方式不同:栈由系统自动分配;堆是自己申请和释放的。
  • 申请大小限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
  • 申请效率不同:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢,且会有碎片。

形象的比喻

栈就像我们去饭馆里吃饭,只管点菜(发出申# # 和吃(使用),吃饱了就走,不必理会# 洗菜等准备工作和# 刷锅等扫尾工作,他的好处是快捷,但是自由度小。

堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

区别以下指针类型?

  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

宏定义和typedef区别?

宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。

宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。

宏不检查类型;typedef会检查数据类型。

宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。

注意对指针的操作,typedef char p_char和define p_char char 区别巨大。

变量声明和定义区别?

声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。

相同变量可以在多处声明(外部变量extern),但只能在一处定义。

哪几种情况必须用到初始化成员列表?

初始化一个const成员。

初始化一个reference成员。

调用一个基类的构造函数,而该函数有一组参数。

调用一个数据成员对象的构造函数,而该函数有一组参数。

a和&a有什么区别?

假设数组int a[10];

1
int (*p)[10] = &a;

a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]

&a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。

(int *)p,此时输出*p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

迭代器失效的情况

以vector为例:

插入元素:

  • 尾后插入:size < capacity时,首迭代器不失效尾迭代失效(未重新分配空间),size == capacity时,所有迭代器均失效(需要重新分配空间)。
  • 中间插入:中间插入:size < capacity时,首迭代器不失效但插入元素之后所有迭代器失效,size == capacity时,所有迭代器均失效。

删除元素:

  • 尾后删除:只有尾迭代失效。
  • 中间删除:删除位置之后所有迭代失效。

C和C++的区别

  • C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
  • 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
  • C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
  • C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
  • 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。
  • C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++允许重复定义变量,C语言也是做不到这一点的
  • 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
  • C++相对与C增加了一些关键字,如:bo# usi# dynamic_ca# namespace等等

C++中struct和class的区别

相同点:

  • 两者都拥有成员# 公有和私有部分
  • 任何可以使用class完成的工作,同样可以使用struct完成

不同点

  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
  • class默认是private继承,而struct模式是public继承
  • class可以作为模板类型,struct不行

引申:C++和C的struct区别

C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)

C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数

C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)

struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

define宏定义和const的区别

编译阶段

define是在编译的预处理阶段起作用,而const是在# 运行的时候起作用

安全性

define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错

const常量有数据类型,编译器可以对其进行类型安全检查

内存占用

define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表

宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。

宏不检查类型;const会检查数据类型。

宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。

final和override关键字

override

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

1
2
3
4
5
6
7
8
9
10
class A
{
virtual void foo();
}
class B : public A
{
void foo(); //OK
virtual void foo(); // OK
void foo() override; //OK
}

如果不使用override,当你手一抖,将foo()写成了foo()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:
1
2
3
4
5
6
7
8
9
class A
{
virtual void foo();
};
class B : public A
{
virtual void f00(); //OK,这个函数是B新增的,不是继承的
virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
};

final

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base
{
virtual void foo();
};

class A : public Base
{
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
void foo() override; // Error: 在A中已经被final了
};

class C : B // Error: B is final
{
};

拷贝初始化和直接初始化

当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下

  • string str1("I am a string");//语句1 直接初始化
  • string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化
  • string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
  • string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数

为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价)。但是需要辨别两种情况。

当拷贝构造函数为private时:语句3和语句4在编译时会报错

使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错

初始化和赋值的区别

对于简单类型来说,初始化和赋值没什么区别

对于类和复杂数据类型来说,这两者的区别就大了,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){};
A(const A& a){};
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){

A a(1,1);
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}

模板函数和模板类的特例化

引入原因

编写单一的模板,它能适应多种类型的需求,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化

定义

对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上

(1)模板函数特例化

必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T> //模板函数
int compare(const T &v1,const T &v2)
{
if(v1 > v2) return -1;
if(v2 > v1) return 1;
return 0;
}
//模板特例化,满足针对字符串特定的比较,要提供所有实参,这里只有一个T
template<>
int compare(const char* const &v1,const char* const &v2)
{
return strcmp(p1,p2);
}

本质

特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如,此处如果是compare(3,5),则调用普通的模板,若为compare(“hi”,”haha”)则调用特例化版本(因为这个cosnt char*相对于T,更匹配实参类型),注意二者函数体的语句不一样了,实现不同功能。

注意

模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。

(2)类模板特例化

原理类似函数模板,不过在类中,我们可以对模板进行特例化,也可以对类进行部分特例化。对类进行特例化时,仍然用template<>表示是一个特例化版本,例如:

1
2
3
4
5
6
7
template<>
class hash<sales_data>
{
size_t operator()(sales_data& s);
//里面所有T都换成特例化类型版本sales_data
//按照最佳匹配原则,若T != sales_data,就用普通类模板,否则,就使用含有特定功能的特例化版本。
};

类模板的部分特例化

不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,哪个最匹配,就用相应的模板)

特例化类中的部分成员

可以特例化类中的部分成员函数而不是整个类,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class Foo
{
void Bar();
void Barst(T a)();
};

template<>
void Foo<int>::Bar()
{
//进行int类型的特例化处理
cout << "我是int型特例化" << endl;
}

Foo<string> fs;
Foo<int> fi;//使用特例化
fs.Bar();//使用的是普通模板,即Foo<string>::Bar()
fi.Bar();//特例化版本,执行Foo<int>::Bar()
//Foo<string>::Bar()和Foo<int>::Bar()功能不同

C和C++的类型安全

什么是类型安全?

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。

如果C++使用得当,它将远比C更有类型安全性。相比于C语言,C++提供了一些新的机制保障类型安全:

  • 操作符new返回的指针类型严格与对象匹配,而不是void*
  • C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
  • 引入const关键字代替define constants,它是有类型有作用域的,define constants只是简单的文本替换

C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。

例1:不同类型指针之间转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
using namespace std;

class Parent{};
class Child1 : public Parent
{
public:
int i;
Child1(int e):i(e){}
};
class Child2 : public Parent
{
public:
double d;
Child2(double e):d(e){}
};
int main()
{
Child1 c1(5);
Child2 c2(4.1);
Parent* pp;
Child1* pc1;

pp=&c1;
pc1=(Child1*)pp; // 类型向下转换 强制转换,由于类型仍然为Child1*,不造成错误
cout<<pc1->i<<endl; //输出:5

pp=&c2;
pc1=(Child1*)pp; //强制转换,且类型发生变化,将造成错误
cout<<pc1->i<<endl;// 输出:1717986918
return 0;
}

上面两个例子之所以引起类型不安全的问题,是因为程序员使用不得当。第一个例子用到了空类型指针void,第二个例子则是在两个类型指针之间进行强制转换。因此,想保证程序的类型安全性,应尽量避免使用空类型指针void,尽量不对两种类型指针做强制转换。

C++有哪几种的构造函数

C++中的构造函数可以分为4类:

  • 默认构造函数
  • 初始化构造函数(有参数)
  • 拷贝构造函数
  • 移动构造函数(move和右值引用)
  • 委托构造函数
  • 转换构造函数

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;

class Student{
public:
Student(){//默认构造函数,没有参数
this->age = 20;
this->num = 1000;
};
Student(int a, int n):age(a), num(n){}; //初始化构造函数,有参数和参数列表
Student(const Student& s){//拷贝构造函数,这里与编译器生成的一致
this->age = s.age;
this->num = s.num;
};
Student(int r){ //转换构造函数,形参是其他类型变量,且只有一个形参
this->age = r;
this->num = 1002;
};
~Student(){}
public:
int age;
int num;
};

int main(){
Student s1;
Student s2(18,1001);
int a = 10;
Student s3(a);
Student s4(s3);

printf("s1 age:%d, num:%d\n", s1.age, s1.num);
printf("s2 age:%d, num:%d\n", s2.age, s2.num);
printf("s3 age:%d, num:%d\n", s3.age, s3.num);
printf("s2 age:%d, num:%d\n", s4.age, s4.num);
return 0;
}
//运行结果
//s1 age:20, num:1000
//s2 age:18, num:1001
//s3 age:10, num:1002
//s2 age:10, num:1002

默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作

复制构造函数用于复制本类的对象

转换构造函数用于将其他类型的变量,隐式转换为本类对象

浅拷贝和深拷贝的区别

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>  
#include <string.h>
using namespace std;

class Student
{
private:
int num;
char *name;
public:
Student(){
name = new char(20);
cout << "Student" << endl;
};
~Student(){
cout << "~Student " << &name << endl;
delete name;
name = NULL;
};
Student(const Student &s){//拷贝构造函数
//浅拷贝,当对象的name和传入对象的name指向相同的地址
name = s.name;
//深拷贝
//name = new char(20);
//memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
};
};

int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
//浅拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***

//深拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0

从执行结果可以看出,浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。

auto decltype和decltype(auto)的用法

(1)auto

C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同,

auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型

//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int*
const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt

//引用和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*

(2)decltype

有的时候我们还会遇到这种情况,我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int func() {return 0};

//普通类型
decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func()
int a = 0;
decltype(a) b = 4; // a的类型是int, 所以b的类型也是int

//不论是顶层const还是底层const, decltype都会保留
const int c = 3;
decltype(c) d = c; // d的类型和c是一样的, 都是顶层const
int e = 4;
const int* f = &e; // f是底层const
decltype(f) g = f; // g也是底层const

//引用与指针类型
//1. 如果表达式是引用类型, 那么decltype的类型也是引用
const int i = 3, &j = i;
decltype(j) k = 5; // k的类型是 const int&

//2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; // 此时是int类型

//3. 对指针的解引用操作返回的是引用类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; // c是int&类型, c和j绑定在一起

//4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
int i = 3;
decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了一起

(3)decltype(auto)

decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:

1
2
3
int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

C++中NULL和nullptr区别

算是为了与C语言进行兼容而定义的一个问题吧

NULL来自C语言,一般由宏定义实现,而 nullptr 则是C++11的新增关键字。在C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数0。编译器一般对其实际定义如下:

1
2
3
4
5
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

在C++中指针必须有明确的类型定义。但是将NULL定义为0带来的另一个问题是无法与整数的0区分。因为C++中允许有函数重载,所以可以试想如下函数定义情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

void fun(char* p) {
cout << "char*" << endl;
}

void fun(int p) {
cout << "int" << endl;
}

int main()
{
fun(NULL);
return 0;
}
//输出结果:int

那么在传入NULL参数时,会把NULL当做整数0来看,如果我们想调用参数是指针的函数,该怎么办呢?。nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。

nullptr的一种实现方式如下:

1
2
3
4
5
6
7
const class nullptr_t{
public:
template<class T> inline operator T*() const{ return 0; }
template<class C, class T> inline operator T C::*() const { return 0; }
private:
void operator&() const;
} nullptr = {};

以上通过模板类和运算符重载的方式来对不同类型的指针进行实例化从而解决了(void*)指针带来参数类型不明的问题,另外由于nullptr是明确的指针类型,所以不会与整形变量相混淆。但nullptr仍然存在一定问题,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

void fun(char* p)
{
cout<< "char* p" <<endl;
}
void fun(int* p)
{
cout<< "int* p" <<endl;
}

void fun(int p)
{
cout<< "int p" <<endl;
}
int main()
{
fun((char*)nullptr);//语句1
fun(nullptr);//语句2
fun(NULL);//语句3
return 0;
}
//运行结果:
//语句1:char* p
//语句2:报错,有多个匹配
//3:int p

在这种情况下存在对不同指针类型的函数重载,此时如果传入nullptr指针则仍然存在无法区分应实际调用哪个函数,这种情况下必须显示的指明参数类型。

简要说明C++的内存分区

C++中的内存分区,分别# # 自由存# 全局/静态存# 常量存储区和代码区。如下图所示

图片

栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限

堆:就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收

自由存储区:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来结束自己的生命的

全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0

常量存储区:这是一块比较特殊的存储区,这里面存放的是常量,不允许修改

代码区:存放函数体的二进制代码

《C/C++内存管理详解》:

https://chenqx.github.io/2014/09/25/Cpp-Memory-Management/

C++的异常处理的方法

在程序执行过程中,由于程序员的疏忽或是系统资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:

数组下标越界

除法计算时除数为0

动态分配空间时空间不足

如果不及时对这些异常进行处理,程序多数情况下都会崩溃。

(1)t# throw和catch关键字

C++中的异常处理机制主要使用t# throw和catch三个关键字,其在程序中的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;
int main()
{
double m = 1, n = 0;
try {
cout << "before dividing." << endl;
if (n == 0)
throw - 1; //抛出int型异常
else if (m == 0)
throw - 1.0; //拋出 double 型异常
else
cout << m / n << endl;
cout << "after dividing." << endl;
}
catch (double d) {
cout << "catch (double)" << d << endl;
}
catch (...) {
cout << "catch (...)" << endl;
}
cout << "finished" << endl;
return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished

代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块。如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。

catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。

当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。

(2)函数的异常声明列表

有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表,写法如下:

int fun() throw(int,double,A,B,C){…};
这种写法表名函数可能会抛出int,double型或# # C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常

(3)C++标准异常类 exception

bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <typeinfo>
using namespace std;

class A{
public:
virtual ~A();
};

using namespace std;
int main() {
A* a = NULL;
try {
cout << typeid(*a).name() << endl; // Error condition
}
catch (bad_typeid){
cout << "Object is NULL" << endl;
}
return 0;
}
//运行结果:bject is NULL

  • bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
  • bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常
  • out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

静态变量什么时候初始化

1) 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

2) 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

3) 而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

值传递、指针传递、引用传递的区别和效率

  • 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)
  • 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)
  • 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)
  • 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

什么是内存池,如何实现

内存池(Memory Pool) 是一种内存分配方式。通常我们习惯直接使用new、malloc 等申请内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块, 若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。

这里简单描述一下《STL源码剖析》中的内存池实现机制:

allocate包装malloc,deallocate包装free

一般是一次20*2个的申请,先用一半,留着一半,为什么也没个说法,侯捷在STL那边书里说好像是C++委员会成员认为20是个比较好的数字,既不大也不小

首先客户端会调用malloc()配置一定数量的区块(固定大小的内存块,通常为8的倍数),假设40个32bytes的区块,其中20个区块(一半)给程序实际使用,1个区块交出,另外19个处于维护状态。剩余20个(一半)留给内存池,此时一共有(20*32byte)

客户端之后有有内存需求,想申请(2064bytes)的空间,这时内存池只有(2032bytes),就先将(10*64bytes)个区块返回,1个区块交出,另外9个处于维护状态,此时内存池空空如也

接下来如果客户端还有内存需求,就必须再调用malloc()配置空间,此时新申请的区块数量会增加一个随着配置次数越来越大的附加量,同样一半提供程序使用,另一半留给内存池。申请内存的时候用永远是先看内存池有无剩余,有的话就用上,然后挂在0-15号某一条链表上,要不然就重新申请。

如果整个堆的空间都不够了,就会在原先已经分配区块中寻找能满足当前需求的区块数量,能满足就返回,不能满足就向客户端报bad_alloc异常

allocator就是用来分配内存的,最重要的两个函数是allocate和deallocate,就是用来申请内存和回收内存的,外部(一般指容器)调用的时候只需要知道这些就够了。内部实现,目前的所有编译器都是直接调用的::operator new()::operator delete(),说白了就是和直接使用new运算符的效果是一样的,所以老师说它们都没做任何特殊处理。

最开始GC2.9之前:

new和 operator new 的区别:new 是个运算符,编辑器会调用 operator new(0)

operator new()里面有调用malloc的操作,那同样的 operator delete()里面有调用的free的操作

GCC2.9的alloc的一个比较好的分配器的实现规则

维护一条0-15号的一共16条链表,其中0表示8 bytes ,1表示 16 bytes,2表示 24bytes。。。。而15 表示 16* 8 = 128bytes,如果在申请时并不是8的倍数,那就找刚好能满足内存大小的那个位置。比如想申请 12,那就是找16了,想申请 20 ,那就找 24 了

但是现在GC4.9及其之后 也还有,变成_pool_alloc这个名字了,不再是默认的了,你需要自己去指定它可以自己指定,比如说vector<string,__gnu_cxx::pool_allocvec;这样来使用它,现在用的又回到以前那种对malloc和free的包装形式了

从汇编层去解释一下引用

1
2
3
4
5
9:      int x = 1;
00401048 mov dword ptr [ebp-4],1
10: int &b = x;
0040104F lea eax,[ebp-4]
00401052 mov dword ptr [ebp-8],eax

x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的,所以b的地址比x的低。

lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器

mov dword ptr [ebp-8],eax这条语句将eax的值放入b的地址

ebp-8中上面两条汇编的作用即:将x的地址存入变量b中,这不和将某个变量的地址存入指针变量是一样的吗?所以从汇编层次来看,的确引用是通过指针来实现的。

C++模板是什么,你知道底层怎么实现的?

1) 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

2) 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。

什么是内存泄露,如何检测与避免

内存泄露

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了

避免内存泄露的几种方式

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
  • 一定要将基类的析构函数声明为虚函数
  • 对象数组的释放一定要用delete []
  • 有new就有delete,有malloc就有free,保证它们一定成对出现

对象复用的了解,零拷贝的了解

对象复用

对象复用其本质是一种设计模式:Flyweight享元模式。

通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。

零拷贝

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

零拷贝技术可以减少数据拷贝和共享总线操作的次数。

在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <vector>
#include <string>
#include <iostream>
using namespace std;

struct Person
{
string name;
int age;
//初始构造函数
Person(string p_name, int p_age): name(std::move(p_name)), age(p_age)
{
cout << "I have been constructed" <<endl;
}
//拷贝构造函数
Person(const Person& other): name(std::move(other.name)), age(other.age)
{
cout << "I have been copy constructed" <<endl;
}
//转移构造函数
Person(Person&& other): name(std::move(other.name)), age(other.age)
{
cout << "I have been moved"<<endl;
}
};

int main()
{
vector<Person> e;
cout << "emplace_back:" <<endl;
e.emplace_back("Jane", 23); //不用构造类对象

vector<Person> p;
cout << "push_back:"<<endl;
p.push_back(Person("Mike",36));
return 0;
}
//输出结果:
//emplace_back:
//I have been constructed
//push_back:
//I have been constructed
//I am being moved.

解释一下什么是trivial destructor

“trivial destructor”一般是指用户没有自定义析构函数,而由系统生成的,这种析构函数在《STL源码解析》中成为“无关痛痒”的析构函数。

反之,用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露

对于trivial destructor,如果每次都进行调用,显然对效率是一种伤害,如何进行判断呢?《STL源码解析》中给出的说明是:

首先利用value_type()获取所指对象的型别,再利用type_traits判断该型别的析构函数是否trivial,若是`(true_type),则什么也不做,若为(__false_type)`,则去调用destory()函数

也就是说,在实际的应用当中,STL库提供了相关的判断方法__type_traits,感兴趣的读者可以自行查阅使用方式。除了trivial destructor,还有trivial construct、trivial copy construct等,如果能够对是否trivial进行区分,可以采用内存处理函数memcpy()、malloc()等更加高效的完成相关操作,提升效率。

C++中类的数据成员和成员函数内存分布情况

C++类是由结构体发展得来的,所以他们的成员变量(C语言的结构体只有成员变量)的内存分配机制是一样的。下面我们以类来说明问题,如果类的问题通了,结构体也也就没问题啦。类分为成员变量和成员函数,我们先来讨论成员变量。

一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。(在定义类对象的同时这些成员变量也就被定义了),举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class Person
{
public:
Person()
{
this->age = 23;
}
void printAge()
{
cout << this->age <<endl;
}
~Person(){}
public:
int age;
};

int main()
{
Person p;
cout << "对象地址:"<< &p <<endl;
cout << "age地址:"<< &(p.age) <<endl;
cout << "对象大小:"<< sizeof(p) <<endl;
cout << "age大小:"<< sizeof(p.age) <<endl;
return 0;
}
//输出结果
//对象地址:0x7fffec0f15a8
//age地址:0x7fffec0f15a8
//对象大小:4
//age大小:4

从代码运行结果来看,对象的大小和对象中数据成员的大小是一致的,也就是说,成员函数不占用对象的内存。这是因为所有的函数都是存放在代码区的,不管是全局函数,还是成员函数。要是成员函数占用类的对象空间,那么将是多么可怕的事情:定义一次类对象就有成员函数占用一段空间。我们再来补充一下静态成员函数的存放问题:静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态数据成员,就像我前面提到的,所有函数都存放在代码区,静态函数也不例外。所有有人一看到 static 这个单词就主观的认为是存放在全局数据区,那是不对的。

析构函数的作用,如何起作用?

1) 构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。

规则,只要你一实例化对象,系统自动回调用一个构造函数就是你不写,编译器也自动调用一次。

2) 析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。

析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。

每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。

构造函数析构函数可否抛出异常

1) C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。

因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。

2) 用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;

3) 如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用terminate函数让程序结束;

4) 如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

类什么时候会析构?

1) 对象生命周期结束,被销毁时;

2) delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;

3) 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

构造函数的几种关键字

default

default关键字可以显式要求编译器生成合成构造函数,防止在调用时相关构造函数类型没有定义而报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class CString
{
public:
CString() = default; //语句1
//构造函数
CString(const char* pstr) : _str(pstr){}
void* operator new() = delete;//这样不允许使用new关键字
//析构函数
~CString(){}
public:
string _str;
};


int main()
{
auto a = new CString(); //语句2
cout << "Hello World" <<endl;
return 0;
}
//运行结果
//Hello World

如果没有加语句1,语句2会报错,表示找不到参数为空的构造函数,将其设置为default可以解决这个问题

delete

delete关键字可以删除构造函数、赋值运算符函数等,这样在使用的时候会得到友善的提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class CString
{
public:
void* operator new() = delete;//这样不允许使用new关键字
//析构函数
~CString(){}
};


int main()
{
auto a = new CString(); //语句1
cout << "Hello World" <<endl;
return 0;
}

在执行语句1时,会提示new方法已经被删除,如果将new设置为私有方法,则会报惨不忍睹的错误,因此使用delete关键字可以更加人性化的删除一些默认方法

1
=0

将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处;当然,也可以为纯虚函数提供定义,不过函数体必须定义在类的外部)

C++函数调用的压栈过程

从代码入手,解释这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

int f(int n)
{
cout << n << endl;
return n;
}

void func(int param1, int param2)
{
int var1 = param1;
int var2 = param2;
printf("var1=%d,var2=%d", f(var1), f(var2));
}

int main(int argc, char* argv[])
{
func(1, 2);
return 0;
}
//输出结果
//2
//1
//var1=1,var2=2

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

说说移动构造函数

1) 我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

2) 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。

所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;

3) 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

C++中将临时变量作为返回值时的处理过程

首先需要明白一件事情,临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是可以被分配给其他变量,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了

C语言里规定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit

由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了。当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系

如果我们需要返回值,一般使用赋值语句就可以了

关于this指针你知道什么?全说出来

this指针是类的指针,指向对象的首地址。

this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。

this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。

this指针的用处

一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候(全局函数,静态函数中不能使用this指针),编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行

this指针的使用

一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;

另外一种情况是当形参数与成员变量名相同时用于区分,如this->n = n (不能写成n = n)

类的this指针有以下特点

(1)this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数为T * const this

如:

1
2
3
4
5
6
7
class A{
public:
int func(int p){}
};
其中,func的原型在编译器看来应该是:

int func(A * const this,int p);

(2)由此可见,this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。如:

1
2
3
4
A a;
a.func(10);
//此处,编译器将会编译成:
A::func(&a,10);

看起来和静态函数没差别,对吗?不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,例如VC通常是通过ecx(计数寄存器)传递this参数的。

几个this指针的易混问题

this指针是什么时候创建的?

this在成员函数的开始执行前构造,在成员的执行结束后清除。

但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new的方式创建对象的话,在堆里分配内存,new操作符通过eax(累加寄存器)返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx,之后构造函数里面怎么处理请看上面的回答

this指针存放在何处?堆、栈、全局变量,还是其他?

this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它们并不是和高级语言变量对应的。

this指针是如何传递类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针又是如何找到“类实例后函数的”?

大多数编译器通过ecx(寄数寄存器)寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。

在call之前,编译器会把对应的对象地址放到eax中。this是通过函数参数的首参来传递的。this指针在调用之前生成,至于“类实例后函数”,没有这个说法。类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那儿,不会跑的

this指针是如何访问类中的变量的?

如果不是类,而是结构体的话,那么,如何通过结构指针来访问结构中的变量呢?如果你明白这一点的话,就很容易理解这个问题了。

在C++中,类和结构是只有一个区别的:类的成员默认是private,而结构是public。

this是类的指针,如果换成结构体,那this就是结构的指针了。

我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?

this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。

每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数?

普通的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。但是,即使是虚函数,如果编译期就能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。正是由于this指针的存在,用来指向不同的对象,从而确保不同对象之间调用相同的函数可以互不干扰

构造函数、拷贝构造函数和赋值操作符的区别

构造函数

对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数

拷贝构造函数

对象不存在,但是使用别的已经存在的对象来进行初始化

赋值运算符

对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class A
{
public:
A()
{
cout << "我是构造函数" << endl;
}
A(const A& a)
{
cout << "我是拷贝构造函数" << endl;
}
A& operator = (A& a)
{
cout << "我是赋值操作符" << endl;
return *this;
}
~A() {};
};

int main()
{
A a1; //调用构造函数
A a2 = a1; //调用拷贝构造函数
a2 = a1; //调用赋值操作符
return 0;
}
//输出结果
//我是构造函数
//我是拷贝构造函数
//我是赋值操作符

静态类型和动态类型以及静态绑定和动态绑定的总结

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

引用是否能实现动态绑定,为什么可以实现?

可以。

引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class Base
{
public:
virtual void fun()
{
cout << "base :: fun()" << endl;
}
};

class Son : public Base
{
public:
virtual void fun()
{
cout << "son :: fun()" << endl;
}
void func()
{
cout << "son :: not virtual function" <<endl;
}
};

int main()
{
Son s;
Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化
s.fun(); //son::fun()
b.fun(); //son :: fun()
return 0;
}

需要说明的是虚函数才具有动态绑定,上面代码中,Son类中还有一个非虚函数func(),这在b对象中是无法调用的,如果使用基类指针来指向子类也是一样的。

全局变量和局部变量有什么区别?

生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载,局部变量则分配在堆栈里面 。

指针加减计算要注意什么?

指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main()
{
int *a, *b, c;
a = (int*)0x500;
b = (int*)0x520;
c = b - a;
printf("%d\n", c); // 8
a += 0x020;
c = b - a;
printf("%d\n", c); // -24
return 0;
}

首先变量a和b都是以16进制的形式初始化,将它们转成10进制分别是1280(516\^2=1280)和1312(516\^2+2*16=1312), 那么它们的差值为32,也就是说a和b所指向的地址之间间隔32个位,但是考虑到是int类型占4位,所以c的值为32/4=8

a自增16进制0x20之后,其实际地址变为1280 + 2164 = 1408,(因为一个int占4位,所以要乘4),这样它们的差值就变成了1312 - 1280 = -96,所以c的值就变成了-96/4 = -24

遇到指针的计算,需要明确的是指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果

怎样判断两个浮点数是否相等?

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。

方法调用的原理(栈、汇编)

1) 机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈;帧栈可以认为是程序栈的一段,它有两个端点,一个标识起始地址,一个标识着结束地址,两个指针结束地址指针esp,开始地址指针ebp;

2) 由一系列栈帧构成,这些栈帧对应一个过程,而且每一个栈指针+4的位置存储函数返回地址;每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。

3) 过程实现

  • 备份原来的帧指针,调整当前的栈帧指针到栈指针位置;
  • 建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存;
  • 使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。
  • 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了
  • 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。
  • 释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理。
  • 恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。
  • 弹出返回地址,跳出当前过程,继续执行调用者的代码。

4) 过程调用和返回指令

  • call指令
  • leave指令
  • ret指令

C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?

1) 指针参数传递本质上是值传递,它所传递的是一个地址值。

值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。

值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。

被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。

因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。

符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

类如何实现只能静态分配和只能动态分配

1) 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建

2) 建立类的对象有两种方式:

  • 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
  • 动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;

3) 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有。

如果想将某个类用作基类,为什么该类必须定义而非声明?

派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。

什么情况会自动生成默认构造函数?

  • 带有默认构造函数的类成员对象,如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器就为该类合成出一个默认构造函数。
    • 不过这个合成操作只有在构造函数真正被需要的时候才会发生;
    • 如果一个类A含有多个成员类对象的话,那么类A的每一个构造函数必须调用每一个成员对象的默认构造函数而且必须按照类对象在类A中的声明顺序进行;
  • 带有默认构造函数的基类,如果一个没有任务构造函数的派生类派生自一个带有默认构造函数基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数;
  • 带有一个虚函数的类
  • 带有一个虚基类的类
  • 合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化。

函数指针?

  • 什么是函数指针?

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。

  • 函数指针的声明方法
1
int (*pf)(const int&, const int&); (1)

上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必须的,否则上面的定义就变成了:

1
int *pf(const int&, const int&); (2)

而这声明了一个函数pf,其返回类型为int *, 带有两个const int&参数。

  • 为什么有函数指针

函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。

  • 一个函数名就是一个指针,它指向函数的代码。一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数;

  • 两种方法赋值:

指针名 = 函数名; 指针名 = &函数名

函数调用过程栈的变化,返回值和参数变量哪个先入栈?

  1. 调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
  2. 调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
  3. 在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
  4. 在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;

你知道printf函数的实现原理是什么吗?

在C/C++中,对函数参数的扫描是从后向前的。

C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。

printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf(“%d,%d”,a,b);(其中a、b都是int型的)的汇编代码.

说一说你了解的关于lambda函数的全部知识

1) 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;

2) 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

3) lambda表达式的语法定义如下:

1
[capture] (parameters) mutable ->return-type {statement};

4) lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

为什么模板类一般都是放在一个h文件中

  • 模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。

所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

  • 在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。

所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。

然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

cout和printf有什么区别?

cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。

cout是有缓冲输出:

1
cout < < "abc " < <endl;

1
cout < < "abc\n ";cout < <flush; 这两个才是一样的.

flush立即强迫缓冲输出。
printf是无缓冲输出。有输出时立即输出

当程序中有函数重载时,函数的匹配原则和顺序是什么?

  • 名字查找

  • 确定候选函数

  • 寻找最佳匹配

定义和声明的区别

如果是指变量的声明和定义
从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。

如果是指函数的声明和定义
声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。
定义:一般在源文件里,具体就是函数的实现过程 写明函数体。

说一下你理解的 ifdef endif代表着什么?

  • 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

  • 条件编译命令最常见的形式为:

#ifdef 标识符
程序段1
#else
程序段2
#endif
它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。
其中#else部分也可以没有,即:

#ifdef
程序段1
#denif

  • 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。

在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。

隐式转换,如何消除隐式转换?

1、C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换

2、C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。在比如,数值和布尔类型的转换,整数和浮点数的转换等。

某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。

3、 基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。自定义对象 子类对象可以隐式的转换为父类对象。

4、 C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

5、如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

迭代器:++it、it++哪个好,为什么

  • 前置返回一个引用,后置返回一个对象

// ++i实现代码为:

1
2
3
4
5
6
7
8
9
int& operator++()

{

*this += 1;

return *this;

}
  • 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
1
2
3
4
5
6
7
8
9
10
11
12
13
//i++实现代码为:                 

int operator++(int)

{

int temp = *this;

++*this;

return temp;

}

C++如何处理多个异常的?

  • C++中的异常情况:
    语法错误(编译错误):比如变量未定义、括号不匹配、关键字拼写错误等等编译器在编译时能发现的错误,这类错误可以及时被编译器发现,而且可以及时知道出错的位置及原因,方便改正。
    运行时错误:比如数组下标越界、系统内存不足等等。这类错误不易被程序员发现,它能通过编译且能进入运行,但运行时会出错,导致程序崩溃。为了有效处理程序运行时错误,C++中引入异常处理机制来解决此问题。

  • C++异常处理机制:
    异常处理基本思想:执行一个函数的过程中发现异常,可以不用在本函数内立即进行处理, 而是抛出该异常,让函数的调用者直接或间接处理这个问题。
    C++异常处理机制由3个模块组成:try(检查)、throw(抛出)、catch(捕获)
    抛出异常的语句格式为:throw 表达式;如果try块中程序段发现了异常则抛出异常。

~cpptry { 可能抛出异常的语句;(检查) } catch(类型名[形参名])//捕获特定类型的异常 { //处理1;} catch(类型名[形参名])//捕获特定类型的异常 { //处理2;} catch(…)//捕获所有类型的异常 { }~

模板和实现可不可以不写在一个文件里面?为什么?

因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。

但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。
《C++编程思想》第15章(第300页)说明了原因:模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,

它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

在成员函数中调用delete this会出现什么问题?对象还可以使用吗?

1、在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

2、为什么是不可预期的问题?

delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题才对啊?这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。

此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

3、 如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

如何在不使用额外空间的情况下,交换两个数?你有几种方法

  • 算术
1
2
3
4
x = x + y;
y = x - y;

x = x - y;
  • 异或
1
2
3
4
x = x^y;// 只能对int,char..
y = x^y;
x = x^y;
x ^= y ^= x;

你知道strcpy和memcpy的区别是什么吗?

  1. 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
  2. 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
  3. 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?

参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针

char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

你知道const char* 与string之间的关系是什么吗?

  • string 是c++标准库里面其中一个,封装了对字符串的操作,实际操作过程我们可以用const char*给string类初始化

  • 三者的转化关系如下所示:

a) string转const char*

1
2
3
string s = “abc”; 

const char* c_s = s.c_str();

b) const char* 转string,直接赋值即可

1
2
const char* c_s = “abc”; 
string s(c_s);

c) string 转char*

1
2
3
4
5
string s = “abc”; 
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());

d) char* 转string

1
2
char* c = “abc”; 
string s(c);

e) const char 转char

1
2
3
const char* cpc = “abc”; 
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);

f) char 转const char,直接赋值即可

1
2
char* pc = “abc”; 
const char* cpc = pc;

为什么拷贝构造函数必须传引用不能传值?

  • 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
  • 参数传递过程到底发生了什么?
    • 将地址传递和值传递统一起来,归根结底还是传递的是”值”(地址也是值,只不过通过它可以找到另一个值)!
  • 值传递:
    • 对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
    • 对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);

如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用

ii)引用传递:
无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).
上述1) 2)回答了为什么拷贝构造函数使用值传递会产生无限递归调用,内存溢出。

拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归。

你知道空类的大小是多少吗?

  • C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
  • C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
  • 带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
  • C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。

this指针调用成员变量时,堆栈会发生什么变化?

当在类的非静态成员函数访问类的非静态成员时,编译器会自动将对象的地址传给作为隐含参数传递给函数,这个隐含参数就是this指针。

即使你并没有写this指针,编译器在链接时也会加上this的,对各成员的访问都是通过this的。

例如你建立了类的多个对象时,在调用类的成员函数时,你并不知道具体是哪个对象在调用,此时你可以通过查看this指针来查看具体是哪个对象在调用。This指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。

你知道静态绑定和动态绑定吗?讲讲?

  • 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。

  • 对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。

  • 静态绑定:绑定的是对象的静态类型,某特性(比如函数依赖于对象的静态类型,发生在编译期。

  • 动态绑定:绑定的是对象的动态类型,某特性(比如函数依赖于对象的动态类型,发生在运行期。

如何设计一个类计算子类的个数?

  • 为类设计一个static静态变量count作为计数器;
  • 类定义结束后初始化count;
  • 在构造函数中对count进行+1;
  • 设计拷贝构造函数,在进行拷贝构造函数中进行count +1,操作;
  • 设计复制构造函数,在进行复制函数中对count+1操作;
  • 在析构函数中对count进行-1;

怎么快速定位错误出现的地方

1、如果是简单的错误,可以直接双击错误列表里的错误项或者生成输出的错误信息中带行号的地方就可以让编辑窗口定位到错误的位置上。

2、对于复杂的模板错误,最好使用生成输出窗口。

多数情况下出发错误的位置是最靠后的引用位置。如果这样确定不了错误,就需要先把自己写的代码里的引用位置找出来,然后逐个分析了。

类对象的大小受哪些因素影响?

  • 类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
  • 内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
  • 虚函数的话,会在类对象插入vptr指针,加上指针大小;
  • 当该该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在在派生类中的空间中,也会对派生类进行扩展。

移动构造函数听说过吗?说说

  • 有时候我们会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
  • 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制;
  • C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;
  • 与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。

这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作;

5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Example6 (Example6&& x) : ptr(x.ptr) 

{

x.ptr = nullptr;

}

// move assignment

Example6& operator= (Example6&& x)

{

delete ptr;

ptr = x.ptr;

x.ptr=nullptr;

return *this;

}

什么时候合成构造函数?都说一说,你知道的都说一下

  • 如果一个类没有任何构造函数,但他含有一个成员对象,该成员对象含有默认构造函数,那么编译器就为该类合成一个默认构造函数,因为不合成一个默认构造函数那么该成员对象的构造函数不能调用;
  • 没有任何构造函数的类派生自一个带有默认构造函数的基类,那么需要为该派生类合成一个构造函数,只有这样基类的构造函数才能被调用;
  • 带有虚函数的类,虚函数的引入需要进入虚表,指向虚表的指针,该指针是在构造函数中初始化的,所以没有构造函数的话该指针无法被初始化;
  • 带有一个虚基类的类

还有一点需要注意的是:

  • 并不是任何没有构造函数的类都会合成一个构造函数
  • 编译器合成出来的构造函数并不会显示设定类内的每一个成员变量

那什么时候需要合成拷贝构造函数呢?

有三种情况会以一个对象的内容作为另一个对象的初值:

  • 对一个对象做显示的初始化操作,X xx = x;
  • 当对象被当做参数交给某个函数时;
  • 当函数传回一个类对象时;

  • 如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;

  • 如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;
  • 如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;
  • 如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;

说一说strcpy、sprintf与memcpy这三个函数的不同之处

  • 操作对象不同
    • strcpy的两个操作对象均为字符串
    • sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串
    • memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  • 执行效率不同
    • memcpy最高,strcpy次之,sprintf的效率最低。
  • 实现功能不同
    • strcpy主要实现字符串变量间的拷贝
    • sprintf主要实现其他数据类型格式到字符串的转化
    • memcpy主要是内存块间的拷贝。

将引用作为函数参数有哪些好处?

  • 传递引用给函数与传递指针的效果是一样的。

这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

  • 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;

而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;

如果传递的是对象,还将调用拷贝构造函数。

因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

  • 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用”*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;

另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

如何阻止一个类被实例化?有哪些方法?

  • 将类定义为抽象基类或者将构造函数声明为private;
  • 不允许类外部创建类对象,只能在类内部创建对象

strcpy函数和strncpy函数的区别?哪个函数更安全?

  • 函数原型
1
2
char* strcpy(char* strDest, const char* strSrc)
char* strncpy(char* strDest, const char* strSrc, int pos)
  • strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。
    strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

  • 如果目标长>指定长>源长,则将源长全部拷贝到目标长,自动加上’\0’
    如果指定长<源长,则将源长中按指定长度拷贝到目标字符串,不包括’\0’ 如果指定长>目标长,运行时错误 ;

你知道回调函数吗?它的作用?

  • 当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;
  • 回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;
  • 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;
  • 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

动态编译与静态编译

  • 静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

  • 动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。

缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

————————————————
版权声明:本文为CSDN博主「zongy17」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43614211/article/details/122105195
————————————————

Cannon和fox算法

Cannon算法:

输入:两个N ∗ N的矩阵A、 B,P个处理器。

输出:若P是完全平方数且N % P = 0,则计算C = A ∗ B并输出。

算法思想:将N ∗ N的矩阵分割成P块,即每行每列均有✔P个分块矩阵,那么每个分块的行列都等于N / ✔P。将这些分块分给P个处理器,即处理器 Pij 管理分块Aij、Bij,并计算对应分块Cij的结果。初始时将分块Aij循环左移i步,分块Bij循环上移j步。接下来是运算过程,计算Aij ∗ Bij并将结果放置到Cij中,计算完成后Aij循环左移一步,Bij循环上移一步,重复这个过程✔P次即可计算出最终的Cij,然后由根处理器收集结果即可。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#include <iostream>
#include <cstdio>
#include <mpi.h>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;

const int n = 5; //n是矩阵大小

int MatrixA[n][n], MatrixB[n][n], MatrixC[n][n]; //三个矩阵 已知A B 计算C=A*B
int block, blocknum; //每个分块的大小(一行有多少元素) blocknum=block*block
int numprocs, sqrnumprocs; //前者为处理器的个数 后者为其根号
int move_size; //=blocknum*sizeof(int) 用于memcpy memset等函数

int* blockA, * blockB, * blockC, * tmpa, * tmpb; //存储 分块矩阵 以及传输数据所需要的缓冲区
int myid, row, col; //处理器ID 把整个矩阵划分成若干个分块矩阵分给其它处理器 则该处理器处理第row行 第clo列的分块矩阵

inline void init_AB()//初始化矩阵 A B
{
srand(time(0));
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
MatrixA[i][j] = rand() % 10;
MatrixB[i][j] = rand() % 10;
}
}
}

inline void send_block_AB()
{
int rowmin, rowmax, colmin, colmax; //记录分块矩阵的范围
for (int i = 0; i < numprocs; i++)
{
rowmin = (i / sqrnumprocs) * block;
rowmax = rowmin + block;
colmin = (i % sqrnumprocs) * block;
colmax = colmin + block;
for (int j = rowmin; j < rowmax; j++)
{
for (int k = colmin; k < colmax; k++)
{
int idx = (j - rowmin) * block + k - colmin; //由于tmp是一维数组 所以要计算当前元素对应的下标
tmpa[idx] = MatrixA[j][k];
tmpb[idx] = MatrixB[j][k];
}
}
if (!i) //0号处理器
{
memcpy(blockA, tmpa, move_size);
memcpy(blockB, tmpb, move_size);
}
else
{ //发送分块矩阵 A B
MPI_Send(tmpa, blocknum, MPI_INT, i, 1, MPI_COMM_WORLD);
MPI_Send(tmpb, blocknum, MPI_INT, i, 2, MPI_COMM_WORLD);
}
}
}

inline int getidx(int row, int col) //通过 分块矩阵的 行row 列col 得到管理它的 处理器ID
{
//row=id/sqrnumprocs col=id%sqrnumprocs
return ((row + sqrnumprocs) % sqrnumprocs) * sqrnumprocs + (col + sqrnumprocs) % sqrnumprocs;
}

inline void init_move() //初始时的移动操作 A中分块(i,j)左移i步 B中分块(i,j)上移j步
{
MPI_Status s;
//发送并接受对应的分块
MPI_Sendrecv(blockA, blocknum, MPI_INT, getidx(row, col - row), 1, tmpa, blocknum, MPI_INT, getidx(row, col + row), 1, MPI_COMM_WORLD, &s);
MPI_Sendrecv(blockB, blocknum, MPI_INT, getidx(row - col, col), 2, tmpb, blocknum, MPI_INT, getidx(row + col, col), 2, MPI_COMM_WORLD, &s);
//拷贝
memcpy(blockA, tmpa, move_size);
memcpy(blockB, tmpb, move_size);
}

inline void cal() //计算过程
{
MPI_Status s;
for (int times = 0; times < sqrnumprocs; times++) //sqrnumprocs次 乘法和累加
{
for (int i = 0; i < block; i++) //c[i][j]=a[i][k]*b[k][j]
{ //c[i][j]=blockC[i * block + j]
for (int j = 0; j < block; j++)
{
int sum = blockC[i * block + j];
for (int k = 0; k < block; k++)
sum += blockA[i * block + k] * blockB[k * block + j];
blockC[i * block + j] = sum;
}
} //每个分块计算完毕后
//A中分块左移1步 B中分块上移1步
MPI_Sendrecv(blockA, blocknum, MPI_INT, getidx(row, col - 1), 1, tmpa, blocknum, MPI_INT, getidx(row, col + 1), 1, MPI_COMM_WORLD, &s);
MPI_Sendrecv(blockB, blocknum, MPI_INT, getidx(row - 1, col), 2, tmpb, blocknum, MPI_INT, getidx(row + 1, col), 2, MPI_COMM_WORLD, &s);
//拷贝
memcpy(blockA, tmpa, move_size);
memcpy(blockB, tmpb, move_size);
}
}

inline void getans() //处理器0 从其余处理器处得到分块矩阵的结果并合并
{
MPI_Status s;
int rowmin, rowmax, colmin, colmax;
//处理器0 可直接得到
for (int i = 0; i < block; i++)
for (int j = 0; j < block; j++)
MatrixC[i][j] = blockC[i * block + j];
//其余的需要 接收
for (int i = 1; i < numprocs; i++)
{
MPI_Recv(blockC, blocknum, MPI_INT, i, 1, MPI_COMM_WORLD, &s);
rowmin = (i / sqrnumprocs) * block; //首行坐标
rowmax = rowmin + block; //最后一行的坐标
colmin = (i % sqrnumprocs) * block; //首列坐标
colmax = colmin + block; //最后一列的坐标
for (int j = rowmin; j < rowmax; j++)
for (int k = colmin; k < colmax; k++)
MatrixC[j][k] = blockC[(j - rowmin) * block + k - colmin];
}
}

inline void print_matrix(int ans[][n]) //输出矩阵
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
printf("%-5d", ans[i][j]);
printf("\n");
}
printf("\n");
}

int main(int argc, char* argv[])
{
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs); //个数
MPI_Comm_rank(MPI_COMM_WORLD, &myid); //ID

clock_t start = clock(); //开始时间

sqrnumprocs = sqrt(numprocs);
if (sqrnumprocs * sqrnumprocs != numprocs || n % sqrnumprocs)
{
if (myid == 0)
{
if (n % sqrnumprocs == 0)
cout << "处理器个数应该为完全平方数!\n";
else
cout << "sqrnumprocs必须整除矩阵大小n!\n";
}
MPI_Finalize();
return 0;
}
block = n/sqrnumprocs; //分块大小
blocknum = block * block; //每个分块的元素总数
move_size = blocknum * sizeof(int);
row = myid / sqrnumprocs; //计算自己处理的分块矩阵的 坐标
col = myid % sqrnumprocs;
blockA = new int[blocknum]; //分配空间
blockB = new int[blocknum];
blockC = new int[blocknum];
tmpa = new int[blocknum];
tmpb = new int[blocknum];
memset(blockC, 0, move_size); //初始化c
if (!myid) //0号处理器
{
init_AB(); //初始化矩阵A B
send_block_AB(); //计算分块矩阵 并将其发送给其余处理器
}
else
{ //接受0号发过来的 分块矩阵
MPI_Status s;
MPI_Recv(blockA, blocknum, MPI_INT, 0, 1, MPI_COMM_WORLD, &s);
MPI_Recv(blockB, blocknum, MPI_INT, 0, 2, MPI_COMM_WORLD, &s);
}
init_move(); //初始时分块矩阵的移动
cal(); //计算过程
if (myid == 0)
{
getans();
cout << "矩阵A为:\n";
print_matrix(MatrixA);
cout << "矩阵B为:\n";
print_matrix(MatrixB);
cout << "矩阵C=A*B为(cannon乘法):\n";
print_matrix(MatrixC);
clock_t end = clock(); //结束时间
cout << "Cannon乘法耗时: " << end - start << "\n";
}
else
{
MPI_Send(blockC, blocknum, MPI_INT, 0, 1, MPI_COMM_WORLD);
}

delete[] blockA;
delete[] blockB;
delete[] blockC;
delete[] tmpa;
delete[] tmpb;
MPI_Barrier(MPI_COMM_WORLD);
MPI_Finalize();
return 0;
}
————————————————
版权声明:本文为CSDN博主「csu_xiji」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiji333/article/details/106713440

Fox算法:

输入、输出、环境都同Cannon算法。

算法思想:分块部分的处理和Cannon算法是一样的。在分完块后,Aii向所在行的其他处理器进行一到多播送,然后处理器将收到的分块A与自己的B块进行乘加运算,计算完成之后自己的分块A保持不变,分块B循环上移一步,如果Aij是上次第i行播送的块,本次选择A[i, j + 1 % ✔P]向所在行的其它处理器进行一到多播送,然后进行乘加运算……进行✔P次乘加运算后即可得到所有的Cij,由根处理器收集结果即可。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#include <iostream>
#include <cstdio>
#include <mpi.h>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;

const int n = 1e3; //n是矩阵大小

int MatrixA[n][n], MatrixB[n][n], MatrixC[n][n]; //三个矩阵 已知A B 计算C=A*B
int block, blocknum; //每个分块的大小(一行有多少元素) blocknum=block*block
int numprocs, sqrnumprocs; //前者为处理器的个数 后者为其根号
int move_size; //=blocknum*sizeof(int) 用于memcpy memset等函数

int* blockA, * blockB, * blockC, * tmpa, * tmpb; //存储 分块矩阵 以及传输数据所需要的缓冲区
int myid, row, col; //处理器ID 把整个矩阵划分成若干个分块矩阵分给其它处理器 则该处理器处理第row行 第clo列的分块矩阵

inline void init_AB()//初始化矩阵 A B
{
srand(time(0));
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
MatrixA[i][j] = rand() % 10;
MatrixB[i][j] = rand() % 10;
}
}
}

inline void send_block_AB()
{
int rowmin, rowmax, colmin, colmax; //记录分块矩阵的范围
for (int i = 0; i < numprocs; i++)
{
rowmin = (i / sqrnumprocs) * block;
rowmax = rowmin + block;
colmin = (i % sqrnumprocs) * block;
colmax = colmin + block;
for (int j = rowmin; j < rowmax; j++)
{
for (int k = colmin; k < colmax; k++)
{
int idx = (j - rowmin) * block + k - colmin; //由于tmp是一维数组 所以要计算当前元素对应的下标
tmpa[idx] = MatrixA[j][k];
tmpb[idx] = MatrixB[j][k];
}
}
if (!i) //0号处理器
{
memcpy(blockA, tmpa, move_size);
memcpy(blockB, tmpb, move_size);
}
else
{ //发送分块矩阵 A B
MPI_Send(tmpa, blocknum, MPI_INT, i, 1, MPI_COMM_WORLD);
MPI_Send(tmpb, blocknum, MPI_INT, i, 2, MPI_COMM_WORLD);
}
}
}

inline int getidx(int row, int col) //通过 分块矩阵的 行row 列col 得到管理它的 处理器ID
{
//row=id/sqrnumprocs col=id%sqrnumprocs
return ((row + sqrnumprocs) % sqrnumprocs) * sqrnumprocs + (col + sqrnumprocs) % sqrnumprocs;
}

inline void cal() //计算过程
{
MPI_Status s;
int send_col_idx = row; //在分块矩阵的视图上看 初始时 需要发送分块矩阵(row,send_col_idx)
int idxmin, idxmax; //记录 需要接收分块的 处理器的id范围
for (int times = 0; times < sqrnumprocs; times++) //sqrnumprocs次 乘法和累加
{
//该处理器处理的分块的坐标为 (row,col)
//所以需要从 处理器 getidx(row,send_idx[row]) 处得到分块A 然后进行乘法累加
if (col == send_col_idx)
{
idxmin = getidx(row, 0);
idxmax = getidx(row, sqrnumprocs - 1);
for (int i = idxmin; i <= idxmax; i++)
{
if (i == myid) //自己就没必要发送了
continue;
MPI_Send(blockA, blocknum, MPI_INT, i, 1, MPI_COMM_WORLD);//发送
}
memcpy(tmpa, blockA, move_size); //直接拷贝到目标位置
}
else //接收分块
{
MPI_Recv(tmpa, blocknum, MPI_INT, getidx(row, send_col_idx), 1, MPI_COMM_WORLD, &s);
}
send_col_idx = (send_col_idx + 1) % sqrnumprocs; //递增列号

for (int i = 0; i < block; i++) //c[i][j]=a[i][k]*b[k][j]
{ //c[i][j]=blockC[i * block + j]
for (int j = 0; j < block; j++)
{
int sum = blockC[i * block + j];
for (int k = 0; k < block; k++)
sum += tmpa[i * block + k] * blockB[k * block + j];
blockC[i * block + j] = sum;
}
} //每个分块计算完毕后
//A中分块保持不动 B中分块上移1步
MPI_Sendrecv(blockB, blocknum, MPI_INT, getidx(row - 1, col), 2, tmpb, blocknum, MPI_INT, getidx(row + 1, col), 2, MPI_COMM_WORLD, &s);
//拷贝
memcpy(blockB, tmpb, move_size);
}
}

inline void getans() //处理器0 从其余处理器处得到分块矩阵的结果并合并
{
MPI_Status s;
int rowmin, rowmax, colmin, colmax;
//处理器0 可直接得到
for (int i = 0; i < block; i++)
for (int j = 0; j < block; j++)
MatrixC[i][j] = blockC[i * block + j];
//其余的需要 接收
for (int i = 1; i < numprocs; i++)
{
MPI_Recv(blockC, blocknum, MPI_INT, i, 1, MPI_COMM_WORLD, &s);
rowmin = (i / sqrnumprocs) * block; //首行坐标
rowmax = rowmin + block; //最后一行的坐标
colmin = (i % sqrnumprocs) * block; //首列坐标
colmax = colmin + block; //最后一列的坐标
for (int j = rowmin; j < rowmax; j++)
for (int k = colmin; k < colmax; k++)
MatrixC[j][k] = blockC[(j - rowmin) * block + k - colmin];
}
}

inline void print_matrix(int ans[][n]) //输出矩阵
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
printf("%-5d", ans[i][j]);
printf("\n");
}
printf("\n");
}

int main(int argc, char* argv[])
{
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs); //个数
MPI_Comm_rank(MPI_COMM_WORLD, &myid); //ID

clock_t start = clock(); //开始时间

sqrnumprocs = sqrt(numprocs);
if (sqrnumprocs * sqrnumprocs != numprocs || n % sqrnumprocs)
{
if (myid == 0)
{
if (n % sqrnumprocs == 0)
cout << "处理器个数应该为完全平方数!\n";
else
cout << "sqrnumprocs必须整除矩阵大小n!\n";
}
MPI_Finalize();
return 0;
}
block = n/sqrnumprocs; //分块大小
blocknum = block * block; //每个分块的元素总数
move_size = blocknum * sizeof(int);
row = myid / sqrnumprocs; //计算自己处理的分块矩阵的 坐标
col = myid % sqrnumprocs;
blockA = new int[blocknum]; //分配空间
blockB = new int[blocknum];
blockC = new int[blocknum];
tmpa = new int[blocknum];
tmpb = new int[blocknum];
memset(blockC, 0, move_size); //初始化c
if (!myid) //0号处理器
{
init_AB(); //初始化矩阵A B
send_block_AB(); //计算分块矩阵 并将其发送给其余处理器
}
else
{ //接受0号发过来的 分块矩阵
MPI_Status s;
MPI_Recv(blockA, blocknum, MPI_INT, 0, 1, MPI_COMM_WORLD, &s);
MPI_Recv(blockB, blocknum, MPI_INT, 0, 2, MPI_COMM_WORLD, &s);
}
cal(); //计算过程
if (myid == 0)
{
getans();
//cout << "矩阵A为:\n";
//print_matrix(MatrixA);
//cout << "矩阵B为:\n";
//print_matrix(MatrixB);
//cout << "矩阵C=A*B为:\n";
//print_matrix(MatrixC);
clock_t end = clock(); //结束时间
cout << "Fox乘法耗时: " << end - start << "\n";
}
else
{
MPI_Send(blockC, blocknum, MPI_INT, 0, 1, MPI_COMM_WORLD);
}

delete[] blockA;
delete[] blockB;
delete[] blockC;
delete[] tmpa;
delete[] tmpb;
MPI_Barrier(MPI_COMM_WORLD);
MPI_Finalize();
return 0;
}
————————————————
版权声明:本文为CSDN博主「csu_xiji」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiji333/article/details/106713440

稀疏矩阵

串行优化

作业提供的稀疏矩阵格式为常见的CSR存储格式。最简单的naive写法,可不做预处理,简单地对每一行进行遍历,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void spmv(dist_matrix_t *mat, const data_t* x, data_t* y) {
int m = mat->global_m;
index_t *r_pos = mat->r_pos;
index_t *c_idx = mat->c_idx;
data_t *values = mat->values;
for (int i = 0; i < m; ++i) {
int p, begin = r_pos[i], end = r_pos[i+1];
data_t s = 0;
for(p = begin; p < end; ++p) {
int j = c_idx[p];
s += values[p] * x[j];
}
y[i] = s;
}
}

向量化编译选项

对于大部分代码,都可以首先“无脑”地加上自动向量化的编译选项,看看效果如何。-O3 -fomit-frame-pointer -march=armv8-a -ffast-math的编译选项加上后,效果还是相当明显的。

循环展开

对于串行程序优化,循环展开是常用的方法。将最内层的p循环按步长为4展开,这种写法(下图中所有注释对应成一套)实际上跟用intrinsics的向量化指令(下图中没有注释的对应成一套,arm v8架构的neon intrinsics指令)是效果等价的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void spmv(dist_matrix_t * mat, const data_t * x, data_t* y) {
int m = mat->global_m;
index_t *r_pos = mat->r_pos;
for (int i = 0; i < m; ++i) {
int cnt = r_pos[i+1] - r_pos[i];
// data_t s0 = 0, s1 = 0, s2 = 0, s3 = 0;
float32x4_t temp, matrix, vector; temp = vdupq_n_f32(0.0); data_t s0 = 0;

int p, max_4p = cnt & (~3);
data_t * values = mat->values + r_pos[i];
index_t * c_idx = mat->c_idx + r_pos[i];
for (p = 0; p < max_4p; p += 4) {
int j0 = c_idx[p ];
int j1 = c_idx[p+1];
int j2 = c_idx[p+2];
int j3 = c_idx[p+3];
// s0 += values[p ] * x[j0];
// s1 += values[p+1] * x[j1];
// s2 += values[p+2] * x[j2];
// s3 += values[p+3] * x[j3];
matrix = vld1q_f32(values + p);
vector = vld1q_lane_f32(x + j0, vector, 0);
vector = vld1q_lane_f32(x + j1, vector, 1);
vector = vld1q_lane_f32(x + j2, vector, 2);
vector = vld1q_lane_f32(x + j3, vector, 3);
temp = vmlaq_f32(temp, matrix, vector);
}
for (; p < cnt; p++) {
int j0 = c_idx[p];
s0 += values[p] * x[j0];
}
// y[i] = s0 + s1 + s2 + s3;
y[i] = s0 + vgetq_lane_f32(temp, 0) + vgetq_lane_f32(temp, 1) + vgetq_lane_f32(temp, 2) + vgetq_lane_f32(temp, 3);
}
}

其实理论上来说,稀疏矩阵计算应该是memory bound的类型,循环展开这种提高SIMD效率的优化应该是起不到什么作用的。但在这里大部分算例效果有提升,小部分算例没什么效果甚至有一点倒退。除了上述两者,还尝试了内存对齐、消除指针别名等常用方法,但用在此处后发现并没有什么明显的效果。

CSRL格式

本部分详细介绍可以参见刘芳芳、杨超等的文章。CSRL格式适用于具有局部性特征的矩阵,通过对该格式的SpMV进行向量化,使A和x的访问和计算都可以采用SIMD intrinsics来完成,提高了访问速度,进而提高性能。

CSRL格式相对于CSR格式的主要改进在于:对稀疏矩阵中列下标连续的非零元段,存储首个非零元的列下标及段长度。

因此需要四个数组( 其中矩阵A是mxn矩阵,有nnz个非零元,有nzseg个非零元段):

  • val[nnz]:记录每个非零元的值
  • jas[nnz]:记录每个非零元段的首个非零元所在的列下标
  • jan[nnz]:记录每个非零元段的段长度
  • ptr[m+1]:记录每行的第一个非零元段的索引,其中ptr[m]=nzseg+1

分块COO格式

COO格式是更为简单直接的格式,对于每个非零元直接存储其行索引、列索引和值,即一个三元组(r_idx, c_idx, values)。虽然看起来比CSR格式要多存许多行索引,但它对于高度稀疏的矩阵而言是有利的。最极端地,对于只有一个非零元的稀疏矩阵,COO格式只需要3个数,而CSR格式需要m+1+2个数(m为矩阵行数)。所以COO格式对于分块后的小矩阵存储较为有利。

更有利的是,当分拆成小矩阵后,小矩阵的维度可能小于65536(uint16_t可覆盖)甚至256(uint8_t可覆盖),则可以使用更低精度的无符号数来存储行索引和列索引(小矩阵内部的索引),需要计算时再加上该小矩阵的偏移量(小矩阵在原矩阵中相对于(0,0)的位置)即可,由此可以节省内存带宽,提高性能。

基于OpenMP并行改写

OpenMP的并行非常直观,直接在对矩阵行的遍历上按行做任务划分和并行。如下图所示,实验发现dynamic的调度策略会非常慢。这大概是因为每一行的非零元不算很多,每个线程很快完成一行的计算,然后根据work-stealing的策略,又向调度方申请新的任务,如此频繁的询问、调度带来较大开销。因此尽可能放大并行任务的粒度(调整chunk值)。经过简单调试,static的策略性能最好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void spmv(dist_matrix_t *mat, const data_t* x, data_t* y) {
int m = mat->global_m;
index_t *r_pos = mat->r_pos;
#pragma omp parallel for schedule(static)
for (int i = 0; i < m; ++i) {
int cnt = r_pos[i+1] - r_pos[i];
data_t s0 = 0, s1 = 0, s2 = 0, s3 = 0;
int p, max_4p = cnt & (~3);
data_t * values = mat->values + r_pos[i];
index_t * c_idx = mat->c_idx + r_pos[i];
for (p = 0; p < max_4p; p += 4) {
int j0 = c_idx[p ];
int j1 = c_idx[p+1];
int j2 = c_idx[p+2];
int j3 = c_idx[p+3];
s0 += values[p ] * x[j0];
s1 += values[p+1] * x[j1];
s2 += values[p+2] * x[j2];
s3 += values[p+3] * x[j3];
}
for (; p < cnt; p++) {
int j0 = c_idx[p];
s0 += values[p] * x[j0];
}
y[i] = s0 + s1 + s2 + s3;
}
}

但即使如此,(在后面与MPI的对比中可见)基于OpenMP并行的效果非常差。大概的原因来自两方面:上述的线程在并行区内启动、调度和销毁的开销;以及线程-线程之间的伪共享。虽然对于向量y的伪共享,已经通过尽可能大的任务粒度、先存局部变量s0,s1,s2,s3最后再写y[i]的措施降低了,但总还是有些影响。

基于MPI并行改写

基于MPI的并行首先要考虑负载均衡的任务划分。由于划分必须要静态的,所以还像OpenMP一样以行来做动态的任务分配(你分几行,我分几行,如此往复)显然是不行的。必须要有合理的负载分配方式。

平均分配矩阵各行显然是不行的,因为可能有的行非零元多,有的行少。因此可用非零元个数来做依据,尽量使每个进程分到的那些行所包括的非零元个数尽可能相近。所以在创建分布式矩阵和向量前,首先要由进程0统计矩阵的非零元信息,做出一个尽可能“公平”的划分。如下图所示。

虽然有一个理论上公平的均摊任务量avg_workload,但实际上不可能总是切得这么精准,使得满足avg_workload的划分刚好落在行与行之间。如果每次总是向下取整(即做得比avg_workload少一点,则最后的那个进程会累积下特别多的任务,导致负载极度不均衡。而如果每次总是向上取整(即做得比avg_workload多一点,则最后的几个进程可能会无任务可做,全程空等,但这总比前者要好得多。为了获得更合理的划分,这里采用均匀随机的方法,即进程按照进程号奇偶,交替地多做一点和少做一点。使得不至于最后有不少的进程无任务可做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
MPI_Bcast(&mat.global_m,   1, MPI_INT, 0, MPI_COMM_WORLD);
MPI_Bcast(&mat.global_nnz, 1, MPI_INT, 0, MPI_COMM_WORLD);
if (p_id == 0) {// 进程0负责记录和分配各个进程计算的范围
p_ibeg = (uint32_t*)malloc(sizeof(uint32_t) * p_num);
p_local_m = (uint32_t*)malloc(sizeof(uint32_t) * p_num);
p_local_nnz = (uint32_t*)malloc(sizeof(uint32_t) * p_num);
assert(mat.r_pos[mat.global_m] == mat.global_nnz);
int avg_workload = mat.global_nnz / p_num;// 尽可能平均分
int ptr_last, ptr_curr = 0;
bool shrink = false;
for (int p = 0; p < p_num; p++) {
// p_ibeg[p] = (p > 0) ? (--ptr_curr) : ptr_curr;
p_ibeg[p] = ptr_curr;
ptr_last = ptr_curr;
while (ptr_curr <= mat.global_m && mat.r_pos[ptr_curr] - mat.r_pos[ptr_last] < avg_workload)
ptr_curr++;
if (ptr_curr <= mat.global_m) {
// 如果ptr_curr还落在有效的范围内
// 此时ptr_curr减一则会比avg_workload小,但直接用avg_workload就会比avg_workload大
// 因此均匀随机地取
if (shrink == true)
ptr_curr--;
shrink = !shrink;
} else {// 后面的进程不再有工作了
for (int remain_p = p+1; remain_p < p_num; remain_p++)
p_ibeg[remain_p] = mat.global_m;
break;
}
}
for (int p = 0; p < p_num; p++) {// 确定每个进程负责计算的局部范围local_m,和实际有的
if (p != p_num - 1) {
p_local_m[p] = p_ibeg[p+1] - p_ibeg[p];
p_local_nnz[p] = mat.r_pos[p_ibeg[p+1]] - mat.r_pos[p_ibeg[p]];
} else {// p_num - 1
p_local_m[p] = mat.global_m - p_ibeg[p];
p_local_nnz[p] = mat.r_pos[mat.global_m] - mat.r_pos[p_ibeg[p]];
}
// printf("p_id: %d, row_beg: %d, work_nnz: %d\n", p, p_ibeg[p], p_local_nnz[p]);
}
// 0号进程负责计算的区域
mat.local_ibeg = 0;
mat.local_m = p_local_m[0];
mat.local_nnz = p_local_nnz[0];
// 告诉其它进程负责计算的区域
for (int p = 1; p < p_num; p++) {
MPI_Send(&p_ibeg[p] , 1, MPI_INT, p, 10, MPI_COMM_WORLD);// tag = 10
MPI_Send(&p_local_m[p] , 1, MPI_INT, p, 110, MPI_COMM_WORLD);// tag = 110
MPI_Send(&p_local_nnz[p], 1, MPI_INT, p, 210, MPI_COMM_WORLD);// tag = 210
MPI_Send(&mat.global_m , 1, MPI_INT, p, 310, MPI_COMM_WORLD);// tag = 310
MPI_Send(&mat.global_nnz, 1, MPI_INT, p, 410, MPI_COMM_WORLD);// tag = 410
}
}

而其它进程接收到0号进程的任务分配后,按照各自的需求来开辟分布式矩阵和向量的内存空间。注意这里向量x仍然是全局的,而向量y可以是局部的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
else {// 其它进程负责接收
MPI_Recv(&mat.local_ibeg, 1, MPI_INT, 0, 10, MPI_COMM_WORLD, &status);// tag = 10
MPI_Recv(&mat.local_m , 1, MPI_INT, 0, 110, MPI_COMM_WORLD, &status);// tag = 110
MPI_Recv(&mat.local_nnz , 1, MPI_INT, 0, 210, MPI_COMM_WORLD, &status);// tag = 210
MPI_Recv(&mat.global_m , 1, MPI_INT, 0, 310, MPI_COMM_WORLD, &status);// tag = 310
MPI_Recv(&mat.global_nnz, 1, MPI_INT, 0, 410, MPI_COMM_WORLD, &status);// tag = 410
// 分配矩阵内存空间
mat.r_pos = (index_t*)malloc(sizeof(index_t) * (mat.local_m + 1));// 按照行数+1分配正向表的r_pos空间
mat.c_idx = (index_t*)malloc(sizeof(index_t) * mat.local_nnz);// 按照整个矩阵的非零元数目分配非零元的列序号的存储空间
mat.values = (data_t*)malloc(sizeof(data_t) * mat.local_nnz);// 按照整个矩阵的非零元数目分配非零元的数据的存储空间
// 分配向量内存空间:注意!右端向量x仍然是全局的!只是结果向量是只开一部分
x = (data_t*)malloc(sizeof(data_t) * mat.global_m);
y = (data_t*)malloc(sizeof(data_t) * mat.local_m);
}

在开辟好内存空间后,由进程0(因为只有它读入了文件中的数据)向其它进程分发数据。此处需要注意因为对矩阵的行做了划分(前面进程的数据相当于抛弃掉了),各个进程记录每行数据存储位置的r_pos需要做一个偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (p_id == 0) {
for (int p = 1; p < p_num; p++) {
// printf(" Sending for %d\n", p);
MPI_Send(&mat.r_pos[p_ibeg[p]] , p_local_m[p] , MPI_UNSIGNED, p, 10, MPI_COMM_WORLD);
MPI_Send(&mat.c_idx[mat.r_pos[p_ibeg[p]]] , p_local_nnz[p], MPI_UNSIGNED, p, 110, MPI_COMM_WORLD);
MPI_Send(&mat.values[mat.r_pos[p_ibeg[p]]], p_local_nnz[p], MPI_DATA, p, 210, MPI_COMM_WORLD);
// MPI_Send(&x[p_ibeg[p]] , p_local_m[p] , MPI_DATA, p, 310, MPI_COMM_WORLD);
MPI_Send(&x[0] , mat.global_m , MPI_DATA, p, 310, MPI_COMM_WORLD);
}
} else {
MPI_Recv(&mat.r_pos[0], mat.local_m , MPI_UNSIGNED, 0, 10, MPI_COMM_WORLD, &status);
MPI_Recv(&mat.c_idx[0], mat.local_nnz, MPI_UNSIGNED, 0, 110, MPI_COMM_WORLD, &status);
MPI_Recv(&mat.values[0], mat.local_nnz, MPI_DATA, 0, 210, MPI_COMM_WORLD, &status);
MPI_Recv(&x[0], mat.global_m , MPI_DATA, 0, 310, MPI_COMM_WORLD, &status);
// 其他进程的数据得到之后需要做个偏移!!!
uint32_t r_pos_0 = mat.r_pos[0];
for (uint32_t i = 0; i < mat.local_m; i++)
mat.r_pos[i] -= r_pos_0;
mat.r_pos[mat.local_m] = mat.local_nnz;// r_pos的最后一个元素指向末尾
}

CSR混合CSRL格式

如前所述,CSRL格式在nzseg/nnz值很小时,性能远胜于CSR格式。但大部分情况下,CSR仍占优。因此在此采用两者混合的格式。注意到在划分为进行分布式数组后,相当于每一个进程都在做一个local_mxglobal_m的矩阵和global_m的向量的乘法,所以它们可以独立地使用CSRL格式,从预处理到计算都是互不干扰的。

这样的好处的优化更能“包裹”住原问题的一些奇性。比如,某个矩阵某些行很稠密、元素连成一片,很适合于CSRL格式;但也有很多行很稀疏,更适合于CSR格式。如果不做行划分,它最后只有一个nzseg/nnz值,做和不做CSRL格式的优化都是一锤子买卖,总会亏欠另一方。而行划分之后,相当于有了#procs个nzseg/nnz值,可以各自局部地决定是否要做CSRL格式的优化,具备了一点“自适应性”。这也是MPI划分相比OpenMP要更优胜的地方。在这里决定是否做CSRL格式优化的nzseg/nnz阈值为0.3,小于0.3则该进程转换成CSRL格式,否则不动。

GPU版本(单卡)

GPU版本的SpMV优化的参考资料远比CPU的丰富。作业也只要求做CPU或GPU中一种,因此这里文字介绍较为简单。各种方法的原理是类似的,采用尽可能紧致的存储格式,节省带宽,提高访存效率,然后再考虑SIMD效率。

naive版本的算法非常直接,对矩阵做一维行划分,每一个cuda thread负责矩阵一行的计算。如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__global__ void spmv_naive_kernel(int m, const uint32_t *r_pos, \
const uint32_t *c_idx, const data_t *values, const data_t *x, data_t *y) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if(i < m) {
int p, begin = r_pos[i], end = r_pos[i+1];
data_t s = y[i];
for(p = begin; p < end; ++p) {
int j = c_idx[p];
s += values[p] * x[j];
}
y[i] = s;
}
}
void spmv(dist_matrix_t *mat, const data_t* x, data_t* y) {
int m = mat->global_m;
dim3 grid_size (ceiling(m, 512), 1, 1);
dim3 block_size (512, 1, 1);
spmv_naive_kernel<<<grid_size, block_size>>>(m, \
mat->gpu_r_pos, mat->gpu_c_idx, mat->gpu_values, x, y);
}

稠密矩阵

编译选项

对于naïve版本的代码,如下所示,不妨先“无脑”地加上-O3 -fomit-frame-pointer -march=armv8-a -ffast-math等编译选项来让编译器尽可能提供些自动向量化的效果。

1
2
3
4
5
6
7
8
9
10
11
12
void square_sgemm (int n, float* A, float* B, float* C) {
/* For each row i of A */
for (int i = 0; i < n; ++i)
/* For each column j of B */
for (int j = 0; j < n; ++j) {
/* Compute C(i,j) */
float cij = C[i+j*n];
for( int k = 0; k < n; k++ )
cij += A[i+k*n] * B[k+j*n];
C[i+j*n] = cij;
}
}

仅仅是如此,在不同规模的算例上性能就已经有2~10倍的提升,n每逢4的倍数便有显著的性能下降,这是cache thrashing导致的。可做半定量分析:课程集群L1 cache为64B/line,4路组相联,256个组,可知地址低6位为Offset,中间8位为Index,高位为Tag。N-way set associativity只是提供了conflict miss时的“容错性”,因此不失一般性,假定为direct-mapped来分析。地址每隔2^14B就会拥有相同的Index而被映射到同一个set上,对于单精度浮点数而言就是4096个数,因此当n满足(n*m)%4096==0时(m=1,2,…,n-1),就会在一轮k维的循环中产生cache conflict miss,m就是冲突发生时两个B元素相隔的行数。因此冲突频率随n增大而增大,当n≥4096时,就是每两次相邻的对B元素读取都会造成冲突。

循环变换

注意到在naïve的代码中,由于矩阵采用列主序的存储方式,因此先行后列的方式来计算C中元素的值,虽然对B元素访存是连续的,但对于C和A矩阵的访存都是不利的。尤其在循环最内维的k维,A[i+k*n]是大跨步跳跃式访存。

因此可以采用对i和j维的循环交换,来发掘数据复用的空间局部性。代码如下所示。

1
2
3
4
5
6
7
8
9
void square_sgemm (int n, float* A, float* B, float* C) {
for (int j = 0; j < n; j++){
for (int i = 0; i < n; i++){
register float b = B[j*n + i];
for (int p = 0; p < n; p++)
C[j*n+p] += A[i*n+p] * b;
}
}
}

相当于按列主序遍历B中元素,对于其中的每个元素b,找到它对应有贡献的C和A中的元素所在的列,进行乘加计算。最内维的p维循环对A和C都是连续的,可以有效利用向量化。由于更改循环后,在整轮最内维的p循环中,b的元素是固定不变的寄存器变量,因此不再出现步骤一中的cache conflict miss,反而是矩阵规模n每逢4的倍数就比相邻的有提升,这是因为n为4的倍数能刚好被向量化指令覆盖,而不会多出额外的数据需要标量运算。

消除指针别名

消除指针别名告诉编译器修改指针指向的内存内容只能经过该指针之手,使编译器有更大优化空间。主要方法是给函数形参中的指针添加__restrict__关键字。其它局部的指针变量在定义时也可用此修饰。

循环展开

将循环展开,同时做多列的乘加操作,即取同行不同列的B矩阵元素b0, b1, b2, b3,均与相同的A列做乘法后加到不同的C列上。代码如下所示,需要注意处理余下不足4的列。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int j, i, p;
for ( j = 0; j < ((n)&(~3)); j+=4)//for each colum j of B
for ( i = 0; i < n; i++){//for each row i of B
register float b0 = B(i,j);
register float b1 = B(i,j+1);
register float b2 = B(i,j+2);
register float b3 = B(i,j+3);
for ( p = 0; p < n; p++){
C(p,j ) += A(p,i) * b0;
C(p,j+1) += A(p,i) * b1;
C(p,j+2) += A(p,i) * b2;
C(p,j+3) += A(p,i) * b3;
}
}
for ( ; j < n; j++)//for each remaining colum j of B
for ( i = 0; i < n; i++){//for each row i of B
register float b0 = B(i,j);
for ( p = 0; p < n; p++)
C(p,j ) += A(p,i ) * b0;
}

实验效果显示选4列为一批做乘加效果较好,而大于4列则效果开始下降。循环展开常见的是对最内层做,优势在于循环开销(如终止条件上的分支和计数器变量的更新)的减少。至于为什么要在最外层循环做展开(而不是最内层循环),需要从访存优化的角度来看。对比上一节《循环变换》中最内层循环只有一句C[jn+p] += A[in+p] b;,展开后此处最内层循环有四句C(p,j ) += A(p,i) b0;。注意,改写后,A(p,i)只需要载入寄存器一次,就能服务于C(p,j ),C(p,j+1),C(p,j+2),C(p,j+3)等的计算;而原来,相同的A[in+p]值需要为每个C[jn+p]加载一次。因此,外层循环的展开将矩阵A元素加载次数减少了nb倍(nb为循环展开的项数,这里是4)。

内存对齐和简单Blocking

利用分块技术提高计算访存比获得更高的性能是常用的优化手段。从之前的代码来看,有三层循环(从外到内依次是j -> i -> p),因此可以在这3个维度上采取分块,分别设为SET_J_BLOCK_SIZE, SET_I_BLOCK_SIZE, SET_P_BLOCK_SIZE。越内维访存越连续,因此设的分块大小更大。此处同时配合内存对齐的手段,是因为对于每一个分块矩阵的乘法,单独将A和B拷贝到一块对齐的连续的内存A_local和B_local中,计算结果存到同样对齐的连续的C_local中。一个好处是A_local和B_local矩阵在拷贝时已经预热,放进了CPU的cache里;另一个好处是在真正计算时,读取和存储都是连续的,提高了cache效率。将一块realMxrealN大小的矩阵拷贝到setMxsetN大小的内存中的代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
float C_local[SET_P_BLOCK_SIZE*SET_J_BLOCK_SIZE] __attribute__((aligned(64)));
float A_local[SET_P_BLOCK_SIZE*SET_I_BLOCK_SIZE] __attribute__((aligned(64)));
float B_local[SET_I_BLOCK_SIZE*SET_J_BLOCK_SIZE] __attribute__((aligned(64)));
static void copy_into_MxN_nopadding(int n, int realM, int realN, int setM, \
const float* __restrict__ array, float* __restrict__ array_local) {
for (int local_col = 0; local_col < realN; local_col++){
for (int local_row = 0; local_row < realM; local_row++)
array_local[local_row] = array[local_row];
array_local += setM;
array += n;
}
}

整体的计算逻辑如下所示,仅做了一级分块,其中计算部分类似前面步骤四中的j以步长4为单位做循环。区别在于分块后为减低寻址开销,每个分块用局部的指针 xxx_local_ptr指示当前计算的位置。拷贝分块矩阵的函数copy_into_MxN_nopadding与步骤六中的函数copy_PxI_nopadding()几乎一样。为了寻找这组最优的分块,可以通过编一个简单的Shell脚本,设置环境变量来指定各维度的分块,然后在Makefile里根据环境变量定义宏,再编译和运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void square_sgemm (int n, float* __restrict__ A, float* __restrict__ B, float* __restrict__ C) {

for (int j_block = 0; j_block < n; j_block += SET_J_BLOCK_SIZE){//对于B而言的水平划分
int REAL_J_BLOCK_SIZE = min(SET_J_BLOCK_SIZE, n - j_block);
for (int i_block = 0; i_block < n; i_block += SET_I_BLOCK_SIZE){//对于B而言的垂直划分
int REAL_I_BLOCK_SIZE = min(SET_I_BLOCK_SIZE, n - i_block);

copy_into_MxN_nopadding(n, REAL_I_BLOCK_SIZE, REAL_J_BLOCK_SIZE, SET_I_BLOCK_SIZE,\
B + j_block*n + i_block, B_local);

for (int p_block = 0; p_block < n; p_block += SET_P_BLOCK_SIZE) {
int REAL_P_BLOCK_SIZE = min(SET_P_BLOCK_SIZE, n - p_block);

copy_into_MxN_nopadding(n, REAL_P_BLOCK_SIZE, REAL_I_BLOCK_SIZE, SET_P_BLOCK_SIZE,\
A + i_block*n + p_block, A_local);

// local_C清零
float * C_local_ptr = C_local;
for (int j = 0; j < REAL_J_BLOCK_SIZE; j++){//拷贝的时候是部分
for (int p = 0; p < REAL_P_BLOCK_SIZE; p++)
C_local_ptr[p] = 0.0;
C_local_ptr += SET_P_BLOCK_SIZE;//而指针前进的时候是全步长!
}

// 计算
float * B_local_ptr = B_local;
float * A_local_ptr = A_local;
C_local_ptr = C_local;
int j;
for ( j = 0; j < ((REAL_J_BLOCK_SIZE)&(~3)); j+=4){//计算的时候是部分
for (int i = 0; i < REAL_I_BLOCK_SIZE; i++){
register float b0 = B_local_ptr[i];
register float b1 = B_local_ptr[SET_I_BLOCK_SIZE + i];
register float b2 = B_local_ptr[2*SET_I_BLOCK_SIZE + i];
register float b3 = B_local_ptr[3*SET_I_BLOCK_SIZE + i];
for (int p = 0; p < REAL_P_BLOCK_SIZE; p++){
C_local_ptr[p] += A_local_ptr[p] * b0;
C_local_ptr[SET_P_BLOCK_SIZE + p] += A_local_ptr[p] * b1;
C_local_ptr[2*SET_P_BLOCK_SIZE + p] += A_local_ptr[p] * b2;
C_local_ptr[3*SET_P_BLOCK_SIZE + p] += A_local_ptr[p] * b3;
}
A_local_ptr += SET_P_BLOCK_SIZE;//而指针前进的时候是全步长!
}
A_local_ptr = A_local;//A重新归位
B_local_ptr += 4*SET_I_BLOCK_SIZE;
C_local_ptr += 4*SET_P_BLOCK_SIZE;
}
for ( ; j < REAL_J_BLOCK_SIZE; j++){//计算的时候是部分
for (int i = 0; i < REAL_I_BLOCK_SIZE; i++){
register float b0 = B_local_ptr[i];
for (int p = 0; p < REAL_P_BLOCK_SIZE; p++){
C_local_ptr[p] += A_local_ptr[p] * b0;
}
A_local_ptr += SET_P_BLOCK_SIZE;//而指针前进的时候是全步长!
}
A_local_ptr = A_local;//A重新归位
B_local_ptr += SET_I_BLOCK_SIZE;
C_local_ptr += SET_P_BLOCK_SIZE;
}

// 计算完拷贝回去
C += j_block*n + p_block;
C_local_ptr = C_local;
for (int j = 0; j < REAL_J_BLOCK_SIZE; j++){//拷贝的时候是部分
for (int p = 0; p < REAL_P_BLOCK_SIZE; p++)
C[p] += C_local_ptr[p];
C += n;
C_local_ptr += SET_P_BLOCK_SIZE;//而指针前进的时候是全步长!
}
C -= (j_block+REAL_J_BLOCK_SIZE)*n + p_block;
}
}
}
}

两级Blocking+转置重组

为了更细致的优化,可以做二级分块,在原有基础上,在拷贝出来的对齐且连续的A_local和B_local内做进一步的分块,每次计算一个KERNEL_SIZE_ROW x KERNEL_SIZE_COL大小的矩阵乘法。需要说明的是,此部分二级分块的内容参考了Github上的代码,改写融入到原有一级分块的框架中。由于使用了arm neon的intrinsics,每次一次性对A_local和C_local内的4个浮点数操作,故在此处拷贝A和B时使用padding 0来补齐原矩阵分块无法填满A_local和B_local的地方。下图在一级分块中调用二级分块的矩阵乘法subblock_sgemm()函数。类似地,下图的二级分块的乘法调用最内核的sgemm_kernel()完成固定大小的KERNEL_SIZE的小矩阵乘法。此处设置KERNEL_SIZE_ROW = KERNEL_SIZE_COL=8.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void subblock_sgemm(int n, int REAL_P_BLOCK_SIZE, int REAL_J_BLOCK_SIZE, \
int REAL_I_BLOCK_SIZE, float * C) {
for (int subj = 0; subj < REAL_J_BLOCK_SIZE; subj += KERNEL_SIZE_COL) {
int subj_block_size = min(KERNEL_SIZE_COL, REAL_J_BLOCK_SIZE - subj);

for (int subp = 0; subp < REAL_P_BLOCK_SIZE; subp += KERNEL_SIZE_ROW) {
int subp_block_size = min(KERNEL_SIZE_ROW, REAL_P_BLOCK_SIZE - subp);

float * const restrict C_ptr = C + subj*n + subp;
if (subp_block_size==KERNEL_SIZE_ROW && subj_block_size==KERNEL_SIZE_COL)
sgemm_kernel(REAL_I_BLOCK_SIZE, A_local + subp*REAL_I_BLOCK_SIZE, \
B_local + subj*REAL_I_BLOCK_SIZE, C_ptr, 1, n);
else{
sgemm_kernel(REAL_I_BLOCK_SIZE, A_local + subp*REAL_I_BLOCK_SIZE, \
B_local + subj*REAL_I_BLOCK_SIZE, C_buffer, 0, KERNEL_SIZE_ROW);
for (int j = 0; j < subj_block_size; j++)
for (int i = 0; i < subp_block_size; i++)
C_ptr[n*j + i] += C_buffer[j*KERNEL_SIZE_ROW + i];
}
}
}
}

void square_sgemm(int n, float* __restrict__ A, float* __restrict__ B, float* __restrict__ C) {
for (int j_block = 0; j_block < n; j_block += SET_J_BLOCK_SIZE){//对于B而言的水平划分
int REAL_J_BLOCK_SIZE = min(SET_J_BLOCK_SIZE, n - j_block);

for (int i_block = 0; i_block < n; i_block += SET_I_BLOCK_SIZE){//对于B而言的垂直划分
int REAL_I_BLOCK_SIZE = min(SET_I_BLOCK_SIZE, n - i_block);
// 拷贝并转置B的子块到local_B
copy_transpose_B_into_IxJ(n, REAL_I_BLOCK_SIZE, REAL_J_BLOCK_SIZE,\
B + j_block*n + i_block, B_local);
for (int p_block = 0; p_block < n; p_block += SET_P_BLOCK_SIZE) {
int REAL_P_BLOCK_SIZE = min(SET_P_BLOCK_SIZE, n - p_block);
// 拷贝A的子块到local_A
copy_A_into_PxI(n, REAL_P_BLOCK_SIZE, REAL_I_BLOCK_SIZE, A + i_block*n + p_block, A_local);
// 子块的乘法
subblock_sgemm(n, REAL_P_BLOCK_SIZE, REAL_J_BLOCK_SIZE, REAL_I_BLOCK_SIZE, &C(p_block, j_block));
}
}
}
}

在内核函数sgemm_kernel中,利用CPU提供的128bits定长寄存器,通过intrinsics指令完成SIMD操作。基本逻辑是

  • 从小分块内存加载到定长寄存器
  • 乘加操作得到结果
  • 结果从寄存器存储回小分块内存
  • 拷回C矩阵或为补齐而设的缓冲区中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static void sgemm_kernel(int REAL_I_BLOCK_SIZE, const float* __restrict__ a, const float* \
__restrict__ b, float* __restrict__ CorCBuffer, int C_direct, int row_CorCBuffer) {

float32x4_t c00 = {0}, c01 = {0}, c02 = {0}, c03 = {0};
float32x4_t c04 = {0}, c05 = {0}, c06 = {0}, c07 = {0};
float32x4_t c40 = {0}, c41 = {0}, c42 = {0}, c43 = {0};
float32x4_t c44 = {0}, c45 = {0}, c46 = {0}, c47 = {0};
for (int l = 0; l < REAL_I_BLOCK_SIZE; l++) {
float32x4_t value_a0 = vld1q_f32(a + KERNEL_SIZE_ROW*l );
float32x4_t value_a4 = vld1q_f32(a + KERNEL_SIZE_ROW*l + 4);
float32x4_t value_b0 = vld1q_f32(b + KERNEL_SIZE_COL*l );
float32x4_t value_b4 = vld1q_f32(b + KERNEL_SIZE_COL*l + 4);
c00 = vmlaq_laneq_f32(c00, value_a0, value_b0, 0);
c01 = vmlaq_laneq_f32(c01, value_a0, value_b0, 1);
c02 = vmlaq_laneq_f32(c02, value_a0, value_b0, 2);
c03 = vmlaq_laneq_f32(c03, value_a0, value_b0, 3);
c04 = vmlaq_laneq_f32(c04, value_a0, value_b4, 0);
c05 = vmlaq_laneq_f32(c05, value_a0, value_b4, 1);
c06 = vmlaq_laneq_f32(c06, value_a0, value_b4, 2);
c07 = vmlaq_laneq_f32(c07, value_a0, value_b4, 3);
c40 = vmlaq_laneq_f32(c40, value_a4, value_b0, 0);
c41 = vmlaq_laneq_f32(c41, value_a4, value_b0, 1);
c42 = vmlaq_laneq_f32(c42, value_a4, value_b0, 2);
c43 = vmlaq_laneq_f32(c43, value_a4, value_b0, 3);
c44 = vmlaq_laneq_f32(c44, value_a4, value_b4, 0);
c45 = vmlaq_laneq_f32(c45, value_a4, value_b4, 1);
c46 = vmlaq_laneq_f32(c46, value_a4, value_b4, 2);
c47 = vmlaq_laneq_f32(c47, value_a4, value_b4, 3);
}
// 存到临时变量
vst1q_f32(tmp , c00);
vst1q_f32(tmp + 4 , c40);
vst1q_f32(tmp + 8 , c01);
vst1q_f32(tmp + 12, c41);
vst1q_f32(tmp + 16, c02);
vst1q_f32(tmp + 20, c42);
vst1q_f32(tmp + 24, c03);
vst1q_f32(tmp + 28, c43);
vst1q_f32(tmp + 32, c04);
vst1q_f32(tmp + 36, c44);
vst1q_f32(tmp + 40, c05);
vst1q_f32(tmp + 44, c45);
vst1q_f32(tmp + 48, c06);
vst1q_f32(tmp + 52, c46);
vst1q_f32(tmp + 56, c07);
vst1q_f32(tmp + 60, c47);
// 拷贝回矩阵C或缓冲区
if (C_direct == 0){
for (int j = 0; j < KERNEL_SIZE_COL; j++)
for (int i = 0; i < KERNEL_SIZE_ROW; i++)
CorCBuffer[j*row_CorCBuffer + i] = 0.0;
}
for (int j = 0; j < KERNEL_SIZE_COL; j++)
for (int i = 0; i < KERNEL_SIZE_ROW; i++)
CorCBuffer[j*row_CorCBuffer + i] += tmp[j*KERNEL_SIZE_ROW + i];
}

值得一提的是,原作者在这里拷贝A和B矩阵时,使元素位置重组,设计得很精妙,使得后续计算时对B_local的访存与A_local保持一致的pattern,连续高效。这部分较为难懂,按个人理解,计算逻辑的示意图如下。下图中setX即为上文提到的SET_X_BLOCK_SIZE,realX即为REAL_X_BLOCK_SIZE,而KernelRow和KernelCol分别为KERNEL_SIZE_ROW和KERNEL_SIZE_COL。

拷贝并重组存储顺序的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static void copy_PxI_nopadding(int n, int REAL_I_BLOCK_SIZE, \
const float* __restrict__ A, float* __restrict__ A_local) {
for (int local_col = 0; local_col < REAL_I_BLOCK_SIZE; local_col++){
for (int local_row = 0; local_row < KERNEL_SIZE_ROW; local_row++)
A_local[local_row] = A(local_row, 0);//相当于把原A中一个块在内存中离散的数据拷贝成A_local中连续的一大片
A_local += KERNEL_SIZE_ROW;
A += n;
}
}
static void copy_A_into_PxI(int n, int REAL_P_BLOCK_SIZE, int REAL_I_BLOCK_SIZE, \
const float* __restrict__ A, float* __restrict__ A_local) {
int part = REAL_P_BLOCK_SIZE / KERNEL_SIZE_ROW;
int remain_rows = REAL_P_BLOCK_SIZE % KERNEL_SIZE_ROW;
for (int pa = 0; pa < part; pa++){
copy_PxI_nopadding(n, REAL_I_BLOCK_SIZE, A, A_local);
A_local += KERNEL_SIZE_ROW * REAL_I_BLOCK_SIZE;
A += KERNEL_SIZE_ROW;//指针指向下一个块
}
if (remain_rows > 0) {//余下还有
for (int local_col = 0; local_col < REAL_I_BLOCK_SIZE; local_col++){
for (int local_row = 0; local_row < remain_rows; local_row++)
A_local[local_row] = A(local_row, 0);
for (int local_row = remain_rows; local_row < KERNEL_SIZE_ROW; local_row++)
A_local[local_row] = 0.0;
A_local += KERNEL_SIZE_ROW;
A += n;
}
}
}

static void copy_transpose_IxJ_nopadding(int n, int REAL_I_BLOCK_SIZE, \
const float* __restrict__ B, float* __restrict__ B_local) {
for (int local_col = 0; local_col < REAL_I_BLOCK_SIZE; local_col++){
for (int local_row = 0; local_row < KERNEL_SIZE_COL; local_row++)
B_local[local_row] = B(0, local_row);
B_local += KERNEL_SIZE_COL;
B += 1;
}
}
static void copy_transpose_B_into_IxJ(int n, int REAL_I_BLOCK_SIZE, int REAL_J_BLOCK_SIZE,\
const float* __restrict__ B, float* __restrict__ B_local) {
int part = REAL_J_BLOCK_SIZE / KERNEL_SIZE_COL;
int remain_cols = REAL_J_BLOCK_SIZE % KERNEL_SIZE_COL;
for (int pa = 0; pa < part; pa++){
copy_transpose_IxJ_nopadding(n, REAL_I_BLOCK_SIZE, B, B_local);
B_local += KERNEL_SIZE_COL * REAL_I_BLOCK_SIZE;
B += KERNEL_SIZE_COL * n;
}
if (remain_cols > 0) {
for (int local_col = 0; local_col < REAL_I_BLOCK_SIZE; local_col++) {
for (int local_row = 0; local_row < remain_cols; local_row++)
B_local[local_row] = B(0, local_row);
for (int local_row = remain_cols; local_row < KERNEL_SIZE_COL; local_row++)
B_local[local_row] = 0.0;
B_local += KERNEL_SIZE_COL;
B += 1;
}
}
}

https://github.com/zongy17/sgemm-serial

Cannon算法

简介

算法流程图

算法设计方法和模式

任务划分

根据矩阵乘法公式中的累加计算的可分离性,将参与计算的两个矩阵分解成p个小矩阵块(共有p个计算节点),每个节点只进行局部的小矩阵乘法,最终计算结束后将局部的小结果矩阵发送回Master节点。

通讯分析

由于算法在下发任务和收集结果的时候采用了主从模式,所以使用了Master-Worker的全局通讯,该部分通讯由于发送方只有一个0号线程,所以无法并行执行,只能串行执行。同时,在迭代进行小矩阵运算时,各计算节点之间也需要交换矩阵,进行了结构化通讯。该部分通讯由于通讯的局部特性,可以并行执行,能够提高效率。

任务组合

每个节点负责一个小矩阵的串行计算,同时负责小矩阵之间的通讯传递。

处理器映射

由于任务的划分个数等于处理器个数,所以在组合任务的同时完成了处理器映射。

Cannon算法采用了主从模式的同时也采用了分而治之的模式。一方面,0号线程作为Master,负责矩阵A和矩阵B以及矩阵C的I/O,也负责小矩阵的分发和结果的聚集。而其他节点作为Worker进行本地的小矩阵串行乘法计算。另一方面,Cannon算法将两个大矩阵的乘法运算分解为若干各小矩阵的乘法运算,最终计算结束后,将计算结果聚集回来,也采用了分而治之的思想。cannon算法不仅实现了矩阵乘法运算的并行化,也减少了分块矩阵乘法的局部存储量,节省了节点的内存开销。

算法复杂度

设计算的是一个n*n的矩阵乘一个n*n的矩阵,共有p个节点,那么Cannon算法的时间复杂度计算如下:

矩阵乘加的时间由于采用了并行化,所以所需时间为: n^3 / p

若不考虑节点延迟时间,设节点之间通讯的启动时间为ti,传输每个数字的时间为tw,则在两个节点间传输一个子矩阵的时间是:ti + n^2*tw / p

所以节点之间传输子矩阵所需的时间为:2*sqrt(p)*(ti + n^2*tw / p)

综上,cannon算法总的所需时间为:2*sqrt(p)*(ti + n^2*tw / p) + n^3 / p

时间复杂度:O(n^3 / p)
空间复杂度:O(n^2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#include<mpi.h>
#include<pthread.h>
#include<math.h>
#include<cstring>
int myrank, p;

// Compute C = A*B. A is a n1*n2 matrix. B is a n2*n3 matrix.
void matmul(double* A, double* B, double* C, int n1, int n2, int n3)//做矩阵乘法,结果累加到C矩阵中(需要保证C矩阵初始化过)
{
int i,j,k;
//简单的串行矩阵乘法
for (i = 0; i < n1; i++) {
for (j = 0; j < n3; j++) {
for (k = 0; k < n2; k++) {
C[i*n3+j]+=A[i*n2+k]*B[k*n3+j];
}
}
}
}


int setup(int argc,char**argv,double** fstreama,double** fstreamb,int* dim)
{
FILE* fha;
FILE* fhb;
int n1,n2,n3;
int re=1;
if (!(fha = fopen(argv[1], "r"))) //打开存储A矩阵的文件
{
printf("Can't open file %s, Errno=%d\n", argv[1], 1);//打开失败输出信息
return -1;
}
if(fread(&n1,sizeof(int),1,fha)==0)//读取矩阵的行数
{
printf("fread error1!\n");
return -1;
}
if(fread(&n2,sizeof(int),1,fha)==0)//读取矩阵的列数
{
printf("fread error2!\n");
return -1;
}
*fstreama = (double *) malloc (n1*n2*sizeof(double));//为矩阵申请内存
if(fread(*fstreama,sizeof(double),n1*n2,fha)==0)//读取矩阵内容
{
printf("fread error3!\n");
return -1;
}

fclose(fha);//关闭矩阵文件

if (!(fhb = fopen(argv[2], "r"))) //打开存储A矩阵的文件
{
printf("Can't open file %s, Errno=%d\n", argv[2], 2);//打开失败输出信息
return -1;
}
if(fread(&n2,sizeof(int),1,fhb)==0)//读取矩阵的行数
{
printf("fread error4!\n");
return -1;
}
if(fread(&n3,sizeof(int),1,fhb)==0)//读取矩阵的列数
{
printf("fread error5!\n");
return -1;
}
*fstreamb = (double *) malloc (n2*n3*sizeof(double));//为矩阵申请内存
if(fread(*fstreamb,sizeof(double),n2*n3,fhb)==0)//读取矩阵内容
{
printf("fread error6!\n");
return -1;
}

fclose(fhb);//关闭矩阵文件
dim[0] = n1;//返回矩阵的大小参数
dim[1] = n2;//返回矩阵的大小参数
dim[2] = n3;//返回矩阵的大小参数
return 0;
}

void scatter_matrix(double* matrixbuf, int rows, int cols, double* local_matrix, int rootp)//将矩阵划分为小矩阵块并分发到各个节点
{
int row, column, i, j, count;
int maxrows_block = (rows + rootp - 1)/rootp;//小A矩阵块行数的最大值
int maxcols_block = (cols + rootp - 1)/rootp;//小矩阵块列数的最大值
double * matrixbuf2 = NULL;//用来格式化原矩阵的缓冲区
MPI_Status status;//返回通信的状态

if(myrank == 0)//0号线程
{
if(!(matrixbuf2 = (double *)malloc(maxcols_block*maxrows_block*rootp*rootp*sizeof(double))))//为缓冲区申请内存
{
printf("Memory allocation failed\n");
}
//将矩阵转化为按块连续存放的形式,方便分发每块小矩阵,同时对于边界没有对齐的小矩阵,补零对齐,方便计算
count = 0;
for (i = 0; i < rootp; i++){
for (j = 0; j < rootp; j++){
if(i!=(rootp-1)&&j==(rootp-1))//特殊处理除了最后一行以外的最后一列
{
for (row = 0; row < maxrows_block; row++){
for (column = 0; column < maxcols_block; column++){
if((j * maxcols_block + column)>=cols)//补零对齐
{
matrixbuf2[count] = 0;
}else{
matrixbuf2[count] = matrixbuf[(i * maxrows_block + row ) * cols +j * maxcols_block + column];
}
count++;
}
}
}else if(i==(rootp-1)&&j!=(rootp-1))//特殊处理除了最后一列以外的最后一行
{
for (row = 0; row < maxrows_block; row++){
for (column = 0; column < maxcols_block; column++){
if((i * maxrows_block + row)>=rows)//补零对齐
{
matrixbuf2[count] = 0;
}else{
matrixbuf2[count] = matrixbuf[(i * maxrows_block + row)*cols + j * maxcols_block + column];
}
count++;
}
}
}else if(i==(rootp-1)&&j==(rootp-1))//特殊处理最后一列最后一行的那个块
{
for (row = 0; row < maxrows_block; row++){
for (column = 0; column < maxcols_block; column++){
if(((j * maxcols_block + column)>=cols) || ((i * maxrows_block + row)>=rows))//补零对齐
{
matrixbuf2[count] = 0;
}else{
matrixbuf2[count] = matrixbuf[(i * maxrows_block + row) * cols + j * maxcols_block + column];
}
count++;
}
}
}else{//普通的块
for (row = 0; row < maxrows_block; row++){
for (column = 0; column < maxcols_block; column++){
matrixbuf2[count] = matrixbuf[(i * maxrows_block + row)*cols + j * maxcols_block + column];
count++;
}
}
}
}
}
if(count!=maxcols_block*maxrows_block*rootp*rootp)//检查是否出错
{
printf("scatter_matrix error!\n");
return ;
}
//将属于本地的那个块留下来
for(i = 0; i < maxrows_block*maxcols_block; i++)
{
local_matrix[i] = matrixbuf2[i];
}
//分发其他块到对应的线程
for(i = 1; i < rootp*rootp; i++)
{
MPI_Send((matrixbuf2 + (i * maxcols_block * maxrows_block)), maxcols_block * maxrows_block, MPI_DOUBLE, i, 0, MPI_COMM_WORLD);
}
} else {//非0号线程
MPI_Recv(local_matrix, maxcols_block * maxrows_block , MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &status);//非零线程接受0线程发送的小矩阵块
}
if(matrixbuf2!=NULL){//释放缓冲区
free(matrixbuf2);
}
return;
}
//A and bufA : n1_block*n2_block; B and bufB : n2_block*n3_block
//进行cannon算法,各个节点在本地进行矩阵乘法,并交换矩阵块,进行循环,直到计算完毕
void cannon(double* A, double* bufA, double* B, double* bufB, double* C, int n1_block, int n2_block, int n3_block, int rootp)
{
MPI_Request send_a_req, send_b_req;
MPI_Status send_a_status, send_b_status, recv_a_status, recv_b_status;
int cycle_count;
int rank_next_a,rank_next_b;
int rank_last_a,rank_last_b;
int curRowP,curColP,i,j;
int tag=0;//表示当前正确数据再A中还是bufA中,0表示在A中,1表示在bufA中
//先初始化各个块,即A_ij循环左移i步,B_ij循环上移j步,C_ij初始化为零

//初始化矩阵C,值全部设为0
for(i=0;i<n1_block;i++)
{
for(j=0;j<n3_block;j++)
{
C[i*n3_block+j] = 0;
}
}

//循环传播小矩阵
curRowP = myrank/rootp;//当前节点所在行
curColP = myrank%rootp;//当前节点所在列

//获得左移i步后的节点号
if((curColP-curRowP)<0)
{
rank_next_a = myrank+rootp-curRowP;
}else{
rank_next_a = myrank-curRowP;
}


//获得上移j步后的节点号
if((curRowP-curColP)<0)
{
rank_next_b = myrank -curColP*rootp + rootp*rootp;
}else{
rank_next_b = myrank -curColP*rootp;
}

//获得接受左移i步的节点号
if((curColP+curRowP)>=rootp)
{
rank_last_a = myrank+curRowP-rootp;
}else{
rank_last_a = myrank+curRowP;
}


//获得接受上移j步的节点号
if((curRowP+curColP)>=rootp)
{
rank_last_b = myrank + curColP*rootp - rootp*rootp;
}else{
rank_last_b = myrank + curColP*rootp;
}

//非阻塞发送矩阵,如果不需要移动,则直接本地memcpy
if(rank_next_a!=myrank)
{
MPI_Isend(A, n1_block*n2_block, MPI_DOUBLE, rank_next_a, 0, MPI_COMM_WORLD, &send_a_req);//非阻塞发送矩阵A,避免死锁
}else
{
memcpy(bufA, A, n1_block*n2_block*sizeof(double));//本地直接memcpy
}
if(rank_next_b!=myrank)
{
MPI_Isend(B, n2_block*n3_block, MPI_DOUBLE, rank_next_b, 0, MPI_COMM_WORLD, &send_b_req);//非阻塞发送矩阵B,避免死锁
}else
{
memcpy(bufB, B, n2_block*n3_block*sizeof(double));//本地直接memcpy
}

//阻塞接受矩阵
if(rank_last_a!=myrank)
{
MPI_Recv(bufA, n1_block*n2_block, MPI_DOUBLE, rank_last_a, 0, MPI_COMM_WORLD, &recv_a_status);//阻塞接受矩阵A
}
if(rank_last_b!=myrank)
{
MPI_Recv(bufB, n2_block*n3_block, MPI_DOUBLE, rank_last_b, 0, MPI_COMM_WORLD, &recv_b_status);//阻塞接受矩阵B
}

//阻塞等待发送矩阵结束
if(rank_next_a!=myrank)
{
MPI_Wait(&send_a_req, &send_a_status);//阻塞发送矩阵A到结束
}
if(rank_next_b!=myrank)
{
MPI_Wait(&send_b_req, &send_b_status);//阻塞发送矩阵B到结束
}

MPI_Barrier(MPI_COMM_WORLD);//同步
tag=1;
if(myrank%rootp==0)//第一列的节点
{
rank_next_a = myrank+rootp-1;
}else{
rank_next_a = myrank-1;
}

if(myrank/rootp==0)//第一行的节点
{
rank_next_b = myrank+rootp*(rootp-1);
}else{
rank_next_b = myrank - rootp;
}

if(myrank%rootp==(rootp-1))//最后一列的节点
{
rank_last_a = myrank-rootp+1;
}else{
rank_last_a = myrank+1;
}

if(myrank/rootp==(rootp-1))//最后一行的节点
{
rank_last_b = myrank-rootp*(rootp-1);
}else{
rank_last_b = myrank + rootp;
}
//循环,每次做当前块的乘加运算,并使得A_ij循环左移1步,B_ij循环上移1步
for(cycle_count = 0; cycle_count < rootp; cycle_count++)
{
if(tag==1)//数据在bufA中
{
matmul(bufA, bufB, C, n1_block, n2_block, n3_block);//做当前节点的矩阵乘法
//循环传播小矩阵
MPI_Isend(bufA, n1_block*n2_block, MPI_DOUBLE, rank_next_a, 0, MPI_COMM_WORLD, &send_a_req);//非阻塞发送矩阵A,避免死锁
MPI_Isend(bufB, n2_block*n3_block, MPI_DOUBLE, rank_next_b, 0, MPI_COMM_WORLD, &send_b_req);//非阻塞发送矩阵B,避免死锁

MPI_Recv(A, n1_block*n2_block, MPI_DOUBLE, rank_last_a, 0, MPI_COMM_WORLD, &recv_a_status);//阻塞接受矩阵A
MPI_Recv(B, n2_block*n3_block, MPI_DOUBLE, rank_last_b, 0, MPI_COMM_WORLD, &recv_b_status);//阻塞接受矩阵B

MPI_Wait(&send_a_req, &send_a_status);//阻塞发送矩阵A到结束
MPI_Wait(&send_b_req, &send_b_status);//阻塞发送矩阵B到结束
tag = 0;
}else{//数据在A中
matmul(A, B, C, n1_block, n2_block, n3_block);//做当前节点的矩阵乘法
//循环传播小矩阵
MPI_Isend(A, n1_block*n2_block, MPI_DOUBLE, rank_next_a, 0, MPI_COMM_WORLD, &send_a_req);//非阻塞发送矩阵A,避免死锁
MPI_Isend(B, n2_block*n3_block, MPI_DOUBLE, rank_next_b, 0, MPI_COMM_WORLD, &send_b_req);//非阻塞发送矩阵B,避免死锁

MPI_Recv(bufA, n1_block*n2_block, MPI_DOUBLE, rank_last_a, 0, MPI_COMM_WORLD, &recv_a_status);//阻塞接受矩阵A
MPI_Recv(bufB, n2_block*n3_block, MPI_DOUBLE, rank_last_b, 0, MPI_COMM_WORLD, &recv_b_status);//阻塞接受矩阵B

MPI_Wait(&send_a_req, &send_a_status);//阻塞发送矩阵A到结束
MPI_Wait(&send_b_req, &send_b_status);//阻塞发送矩阵B到结束
tag = 1;
}
MPI_Barrier(MPI_COMM_WORLD);//同步
}
return;
}

//gather_matrix((double*)(fstreamc + sizeof(int)*2), n1, n3, C, rootp);
//将各个节点的小矩阵C收集到0号节点
void gather_matrix(double* matrixCbuf, int rows, int cols, double* local_C, int rootp, int rows_block_pad, int cols_block_pad)
{
int curRow, curCol, i, j, curP;
MPI_Status status;
double * matrixC_pad = NULL;//有零填充的矩阵C
if(myrank == 0) {//0号线程
if(!(matrixC_pad = (double *)malloc(rows_block_pad*cols_block_pad*rootp*rootp*sizeof(double))))//为缓冲区申请内存
{
printf("Memory allocation failed\n");
}
//将本地计算结果直接复制过来
for(i = 0; i < rows_block_pad * cols_block_pad; i++){
matrixC_pad[i] = local_C[i];
}
//接受其他非0线程的计算结果
for(i = 1; i < rootp*rootp; i++){
MPI_Recv(matrixC_pad + (i * rows_block_pad * cols_block_pad), rows_block_pad * cols_block_pad, MPI_DOUBLE, i, 0,MPI_COMM_WORLD, &status);
}

//重新整理矩阵C,除去零填充,并且重新整理顺序
for(i=0;i<rows;i++)
{
for(j=0;j<cols;j++)
{
curP = (i/rows_block_pad)*rootp+(j/cols_block_pad);//属于第几个节点,从0开始
curRow = i%rows_block_pad;//属于小矩阵的第几行
curCol = j%cols_block_pad;//属于小矩真的第几列
matrixCbuf[i * cols + j] = matrixC_pad[curP * rows_block_pad * cols_block_pad +curRow*cols_block_pad+curCol];
}
}
} else {//非0号线程
MPI_Send(local_C,rows_block_pad * cols_block_pad, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);//给0号线程发送计算结果
}
if(matrixC_pad!=NULL)
{
free(matrixC_pad);//释放缓冲区
}
return ;
}
int main(int argc, char** argv)
{
double elapsed_time;
// Suppose A:n1xn2, B:n2xn3. n1~n3 are read from input files
int n1, n2, n3,rootp;
// Buffers for matrix A, B, C. Because A, B will be shifted, so they each have two buffers
double *A, *B, *C, *bufA, *bufB;
// On proc 0, buffers to cache matrix files of A, B and C
double *fstreama, *fstreamb;
char *fstreamc;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
MPI_Comm_size(MPI_COMM_WORLD, &p);
rootp = sqrt(p);
if (p != rootp*rootp) {
printf("Processor number must be a square!\n");
}
// On proc 0, preprocess the command line, read in files for A, B and
// put their sizes in dim[].
int dim[3];
if (myrank == 0) {//0号线程负责从文件中读取矩阵A和B以及他们的大小信息
if (setup(argc, argv, &fstreama, &fstreamb, dim)!=0) {
MPI_Finalize(); // Something error during preprocessing
exit(-1);
}
}
MPI_Bcast(dim, 3, MPI_INT, 0, MPI_COMM_WORLD);//0号线程将A和B矩阵的size广播给所有线程
n1 = dim[0];//A: n1*n2
n2 = dim[1];//B: n2*n3
n3 = dim[2];

// Allocate memories for A, B, C, bufA and bufB.
// Suppose an m*n matrix is 2D block-distributed on a rootp*rootp processor grid.
// If rootp doesn't divide m or n, then submatrixes won't have the same size.
// Because we will shift A, B, so we allocate memories according to the max
// rows and cols of A and B.
//因为有可能rootp不能整除n1,n2,n3,所以在申请内存的时候考虑最大的块的大小
int maxrows_a = (n1 + rootp - 1)/rootp;//A矩阵块行数的最大值
int maxcols_a = (n2 + rootp - 1)/rootp;//A矩阵块列数的最大值
int maxrows_b = maxcols_a;//B矩阵块行数的最大值
int maxcols_b = (n3 + rootp - 1)/rootp;//B矩阵块列数的最大值
int bufA_size = sizeof(double)*maxrows_a*maxcols_a;//大小为一个A矩阵块的大小
int bufB_size = sizeof(double)*maxrows_b*maxcols_b;//大小为一个B矩阵块的大小
int bufC_size = sizeof(double)*maxrows_a*maxcols_b;//大小为一个C矩阵块的大小
char* buf;
int i;
if(!(buf = (char *)malloc(bufA_size*2 + bufB_size*2 + bufC_size)))//申请两个A矩阵块,两个B矩阵块,和一个C矩阵块
{
printf("Memory allocation failed\n");
}
//或者以下4个缓存区的指针位置
A = (double*)buf;
bufA = (double*) (buf + bufA_size);
B = (double*) (buf + bufA_size*2);
bufB = (double*) (buf + bufA_size*2 + bufB_size);
C = (double*) (buf + bufA_size*2 + bufB_size*2);
// Proc 0 scatters A, B to other procs in a 2D block distribution fashion
scatter_matrix((double*)fstreama, n1, n2, A, rootp);//0号线程分发A矩阵块到各个线程
MPI_Barrier(MPI_COMM_WORLD);//同步
scatter_matrix((double*)fstreamb, n2, n3, B, rootp);//0号线程分发B矩阵块到各个线程
MPI_Barrier(MPI_COMM_WORLD);//同步
elapsed_time = MPI_Wtime();//记录计算开始的时间戳
// Compute C=A*B by Cannon algorithm
cannon(A, bufA, B, bufB, C, maxrows_a,maxcols_a,maxcols_b, rootp);
MPI_Barrier(MPI_COMM_WORLD);//同步
elapsed_time = MPI_Wtime() - elapsed_time;//记录计算所用的时间
// Proc 0 gathers C from other procs and write it out
FILE* fhc;
int fsizec = sizeof(int)*2 + sizeof(double)*n1*n3;//存储C矩阵以及两个大小参数的空间大小
if(myrank == 0)
{
if (!(fhc = fopen(argv[3], "w"))) //打开输出C矩阵的文件
{
printf("Can't open file %s, Errno=%d\n", argv[3], 3);//打开失败输出信息
MPI_Finalize();
}
fstreamc = (char *)malloc(fsizec);//申请存储矩阵C的内存空间
((int*)fstreamc)[0] = n1;//记录矩阵C的行数
((int*)fstreamc)[1] = n3;//记录矩阵C的列数
}
gather_matrix((double*)(fstreamc + sizeof(int)*2), n1, n3, C, rootp, maxrows_a, maxcols_b);//聚集计算结果,其他线程将自己的C矩阵块发送给线程0
MPI_Barrier(MPI_COMM_WORLD); // Make sure proc 0 read all it needs
if(myrank == 0)
{
printf("Cannon algrithm: multiply a %dx%d with a %dx%d, use %.2f(s)\n",n1, n2, n2, n3, elapsed_time);
fwrite(fstreamc, sizeof(char), fsizec, fhc);//线程0将矩阵C写入文件
fclose(fhc);//关闭文件
free(fstreama);//释放内存
free(fstreamb);//释放内存
free(fstreamc);//释放内存
}
free(buf);//释放存储小矩阵块的内存空间
MPI_Finalize();
return 0;
}

Stencil计算

串行程序优化

对于naïve版本的代码,不妨先“无脑”地加上-O3 -fomit-frame-pointer -march=armv8-a -ffast-math等编译选项来让编译器尽可能提供些自动向量化的效果。仅仅是如此,在不同规模的算例上性能就已经有3~5倍的提升,如下图中的VEC图例所示。

再修改benchmark.c内容对开辟的内存加上内存对齐的声明,如下图中ALIGNED图例所示,并无什么变化。从StackOverflow上了解到分配内存时使用的MPI_Alloc_mem函数(在一些实现中)可能已经做了内存对齐,故效果没有提升也合理。

进一步根据Gabriel Rivera等人写的Tiling Optimizations for 3D Scientific Computations,实行分块策略。按照Tiling的方法,逻辑和伪代码如左图所示,在固定的的x-y分区上逐层向上计算,每次先将该x-y分区内的Stencil计算完毕,再移动至下一个x-y分区,目的是每次换层的时候只需将3层a0中的一层替换出L1 cache,在有限的cache容量内尽量提高数据的可复用性。经过简单实验,得到最优的分块大小为X XX=256, Y YY=8。

除此以外,还可利用指针定位读写的位置,避免计算指标INDEX(…)时相互类似的大量计算。如下图所示


MPI并行

MPI并行模型使用分离的地址空间,因此每个进程做的计算互不干扰,主要需考虑通信带来的开销。由于有3个维度,对进程进行计算任务划分时有多种选择,因此首先从一维划分开始考虑。综合考虑实现复杂性和性能表现,使用MPI的Subarray type来组织和管理halo区的通信。说明,为使负载均衡,以下所有的划分都力求每个进程负责计算的区域大小相等,因此不能整除算例规模的划分方式不予考虑。在跨节点测试时,性能有一定波动,结果取多次测试中的最高值。此部分测试文件见mpi-benchmark.sh和mpi-test.sh文件。

一维z轴划分

将z轴等距划分给n p npnp个进程,每个进程负责n x ∗ n y ∗ ( n z / n p ) nxny(nz/np)nx∗ny∗(nz/np)的任务量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <mpi.h>
const char* version_name = "A mpi version with 1D partition in z";
#include "common.h"

#ifndef SET_Y_SIZE
#define SET_Y_SIZE 8
#endif
#ifndef SET_X_SIZE
#define SET_X_SIZE 256
#endif
#define MIN(a,b) ((a) < (b) ? (a) : (b))

MPI_Comm cart_comm;
int up_ngb, down_ngb;// z小的为down
MPI_Datatype up_send_subarray, up_recv_subarray;
MPI_Datatype down_send_subarray, down_recv_subarray;
MPI_Status status;

// #define useINDEX

// 创建分布式网格:可以根据7点或27点类型做不同的划分
void create_dist_grid(dist_grid_info_t *grid_info, int stencil_type) {
// 一维划分 沿z轴切
if (grid_info->p_id == 0)
printf(" 1D partition: num_proc_z: %d\n", grid_info->p_num);
grid_info->local_size_x = grid_info->global_size_x;
grid_info->local_size_y = grid_info->global_size_y;
if(grid_info->global_size_z % grid_info->p_num != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_z, grid_info->p_num);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_z = grid_info->global_size_z / grid_info->p_num;

grid_info->offset_x = 0;
grid_info->offset_y = 0;
grid_info->offset_z = grid_info->local_size_z * grid_info->p_id;
grid_info->halo_size_x = 1;
grid_info->halo_size_y = 1;
grid_info->halo_size_z = 1;

// printf("pid: %d global: %d %d %d local : %d %d %d offset: %d %d %d\n", grid_info->p_id,\
// grid_info->global_size_z, grid_info->global_size_y, grid_info->global_size_x,\
// grid_info->local_size_z, grid_info->local_size_y, grid_info->local_size_x,\
// grid_info->offset_z, grid_info->offset_y, grid_info->offset_x);

// 创建通信的拓扑
int dims[1] = {grid_info->p_num};
int periods = 0;
MPI_Cart_create(MPI_COMM_WORLD, 1, dims, &periods, 0, &cart_comm);
MPI_Cart_shift(cart_comm, 0, 1, &down_ngb, &up_ngb);
// printf("pid: %d down: %d up: %d\n", grid_info->p_id, down_ngb, up_ngb);

// 创建subarray
int size[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->local_size_x + 2*grid_info->halo_size_x};
int subsize[3] = { grid_info->halo_size_z, \
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->local_size_x + 2*grid_info->halo_size_x};
int start[3];
// send to down_ngb
start[0] = grid_info->halo_size_z; start[1] = 0; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &down_send_subarray);
MPI_Type_commit(&down_send_subarray);
// printf("pid: %d down_send start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// recv from down_ngb
start[0] = 0; start[1] = 0; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &down_recv_subarray);
MPI_Type_commit(&down_recv_subarray);
// printf("pid: %d down_recv start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// send to up_ngb
start[0] = grid_info->local_size_z; start[1] = 0; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &up_send_subarray);
MPI_Type_commit(&up_send_subarray);
// printf("pid: %d up_send start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// recv from up_ngb
start[0] = grid_info->local_size_z + grid_info->halo_size_z; start[1] = 0; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &up_recv_subarray);
MPI_Type_commit(&up_recv_subarray);
// printf("pid: %d up_recv start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);
}

void destroy_dist_grid(dist_grid_info_t *grid_info) {
for (int i = 1; i <= 8; i++) {
if (send_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&send_subarray[i]);
if (recv_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&recv_subarray[i]);
}
}

ptr_t stencil_7(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
double t_last, t_curr;

for(int t = 0; t < nt; ++t) {
cptr_t a0 = buffer[t % 2];
ptr_t a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
t_curr = MPI_Wtime();
MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, grid_info->p_id ^ down_ngb,\
a0, 1, up_recv_subarray, up_ngb, grid_info->p_id ^ up_ngb , cart_comm, &status);
MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, grid_info->p_id ^ up_ngb ,\
a0, 1, down_recv_subarray, down_ngb, grid_info->p_id ^ down_ngb, cart_comm, &status);
// MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, 10,\
// a0, 1, up_recv_subarray, up_ngb, 10, cart_comm, &status);
// MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, 11,\
// a0, 1, down_recv_subarray, down_ngb, 11, cart_comm, &status);
t_last = t_curr;
t_curr = MPI_Wtime();
*comm_time += t_curr - t_last;

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x]\
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x];

}// x loop
}// y loop
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z loop
}
}
t_last = t_curr;
t_curr = MPI_Wtime();
*calc_time += t_curr - t_last;
}
return buffer[nt % 2];
}


ptr_t stencil_27(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
double t_last, t_curr;

for(int t = 0; t < nt; ++t) {
cptr_t restrict a0 = buffer[t % 2];
ptr_t restrict a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
t_curr = MPI_Wtime();
MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, grid_info->p_id ^ down_ngb,\
a0, 1, up_recv_subarray, up_ngb, grid_info->p_id ^ up_ngb , cart_comm, &status);
MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, grid_info->p_id ^ up_ngb ,\
a0, 1, down_recv_subarray, down_ngb, grid_info->p_id ^ down_ngb, cart_comm, &status);
// MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, 10,\
// a0, 1, up_recv_subarray, up_ngb, 10, cart_comm, &status);
// MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, 11,\
// a0, 1, down_recv_subarray, down_ngb, 11, cart_comm, &status);
t_last = t_curr;
t_curr = MPI_Wtime();
*comm_time += t_curr - t_last;

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x] \
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x] \
+ ALPHA_NNZ * a0_local_Z[(y-1)*ldx+x-1] \
+ ALPHA_PNZ * a0_local_Z[(y-1)*ldx+x+1] \
+ ALPHA_NPZ * a0_local_Z[(y+1)*ldx+x-1] \
+ ALPHA_PPZ * a0_local_Z[(y+1)*ldx+x+1] \
+ ALPHA_NZN * a0_local_N[y*ldx+x-1] \
+ ALPHA_PZN * a0_local_N[y*ldx+x+1] \
+ ALPHA_NZP * a0_local_P[y*ldx+x-1] \
+ ALPHA_PZP * a0_local_P[y*ldx+x+1] \
+ ALPHA_ZNN * a0_local_N[(y-1)*ldx+x] \
+ ALPHA_ZPN * a0_local_N[(y+1)*ldx+x] \
+ ALPHA_ZNP * a0_local_P[(y-1)*ldx+x] \
+ ALPHA_ZPP * a0_local_P[(y+1)*ldx+x] \
+ ALPHA_NNN * a0_local_N[(y-1)*ldx+x-1] \
+ ALPHA_PNN * a0_local_N[(y-1)*ldx+x+1] \
+ ALPHA_NPN * a0_local_N[(y+1)*ldx+x-1] \
+ ALPHA_PPN * a0_local_N[(y+1)*ldx+x+1] \
+ ALPHA_NNP * a0_local_P[(y-1)*ldx+x-1] \
+ ALPHA_PNP * a0_local_P[(y-1)*ldx+x+1] \
+ ALPHA_NPP * a0_local_P[(y+1)*ldx+x-1] \
+ ALPHA_PPP * a0_local_P[(y+1)*ldx+x+1];
}// x
}// y
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z
}
}
t_last = t_curr;
t_curr = MPI_Wtime();
*calc_time += t_curr - t_last;
}
return buffer[nt % 2];
}

一维y轴划分

将y轴等距划分给n p npnp个进程,每个进程负责n x ∗ ( n y / n p ) ∗ n z nx(ny/np)nznx∗(ny/np)∗nz的任务量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <mpi.h>
const char* version_name = "A mpi version with 1D partition in y";
#include "common.h"

#ifndef SET_Y_SIZE
#define SET_Y_SIZE 8
#endif
#ifndef SET_X_SIZE
#define SET_X_SIZE 256
#endif
#define MIN(a,b) ((a) < (b) ? (a) : (b))

MPI_Comm cart_comm;
int up_ngb, down_ngb;// y小的为down
MPI_Datatype up_send_subarray, up_recv_subarray;
MPI_Datatype down_send_subarray, down_recv_subarray;
MPI_Status status;

// #define useINDEX

// 创建分布式网格:可以根据7点或27点类型做不同的划分
void create_dist_grid(dist_grid_info_t *grid_info, int stencil_type) {
// 一维划分 沿y轴切
if (grid_info->p_id == 0)
printf(" 1D partition: num_proc_y: %d\n", grid_info->p_num);
grid_info->local_size_x = grid_info->global_size_x;
if(grid_info->global_size_y % grid_info->p_num != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_y, grid_info->p_num);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_y = grid_info->global_size_y / grid_info->p_num;
grid_info->local_size_z = grid_info->global_size_z;

grid_info->offset_x = 0;
grid_info->offset_y = grid_info->local_size_y * grid_info->p_id;
grid_info->offset_z = 0;
grid_info->halo_size_x = 1;
grid_info->halo_size_y = 1;
grid_info->halo_size_z = 1;

// printf("pid: %d global: %d %d %d local : %d %d %d offset: %d %d %d\n", grid_info->p_id,\
// grid_info->global_size_z, grid_info->global_size_y, grid_info->global_size_x,\
// grid_info->local_size_z, grid_info->local_size_y, grid_info->local_size_x,\
// grid_info->offset_z, grid_info->offset_y, grid_info->offset_x);

// 创建通信的拓扑
int dims[1] = {grid_info->p_num};
int periods = 0;
MPI_Cart_create(MPI_COMM_WORLD, 1, dims, &periods, 0, &cart_comm);
MPI_Cart_shift(cart_comm, 0, 1, &down_ngb, &up_ngb);
// printf("pid: %d down: %d up: %d\n", grid_info->p_id, down_ngb, up_ngb);

// 创建subarray
int size[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->local_size_x + 2*grid_info->halo_size_x};
int subsize[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->halo_size_y, \
grid_info->local_size_x + 2*grid_info->halo_size_x};
int start[3];
// send to down_ngb
start[0] = 0; start[1] = grid_info->halo_size_y; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &down_send_subarray);
MPI_Type_commit(&down_send_subarray);
// printf("pid: %d down_send start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// recv from down_ngb
start[0] = 0; start[1] = 0; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &down_recv_subarray);
MPI_Type_commit(&down_recv_subarray);
// printf("pid: %d down_recv start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// send to up_ngb
start[0] = 0; start[1] = grid_info->local_size_y; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &up_send_subarray);
MPI_Type_commit(&up_send_subarray);
// printf("pid: %d up_send start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// recv from up_ngb
start[0] = 0; start[1] = grid_info->local_size_y + grid_info->halo_size_y; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &up_recv_subarray);
MPI_Type_commit(&up_recv_subarray);
// printf("pid: %d up_recv start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);
}

void destroy_dist_grid(dist_grid_info_t *grid_info) {
for (int i = 1; i <= 8; i++) {
if (send_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&send_subarray[i]);
if (recv_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&recv_subarray[i]);
}
}

ptr_t stencil_7(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
double t_last, t_curr;

for(int t = 0; t < nt; ++t) {
cptr_t a0 = buffer[t % 2];
ptr_t a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
t_curr = MPI_Wtime();
MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, grid_info->p_id ^ down_ngb,\
a0, 1, up_recv_subarray, up_ngb, grid_info->p_id ^ up_ngb , cart_comm, &status);
MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, grid_info->p_id ^ up_ngb ,\
a0, 1, down_recv_subarray, down_ngb, grid_info->p_id ^ down_ngb, cart_comm, &status);
// MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, 10,\
// a0, 1, up_recv_subarray, up_ngb, 10, cart_comm, &status);
// MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, 11,\
// a0, 1, down_recv_subarray, down_ngb, 11, cart_comm, &status);
t_last = t_curr;
t_curr = MPI_Wtime();
*comm_time += t_curr - t_last;


for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x]\
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x];

}// x loop
}// y loop
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z loop
}
}
t_last = t_curr;
t_curr = MPI_Wtime();
*calc_time += t_curr - t_last;
}
return buffer[nt % 2];
}


ptr_t stencil_27(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
double t_last, t_curr;

for(int t = 0; t < nt; ++t) {
cptr_t restrict a0 = buffer[t % 2];
ptr_t restrict a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
t_curr = MPI_Wtime();
MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, grid_info->p_id ^ down_ngb,\
a0, 1, up_recv_subarray, up_ngb, grid_info->p_id ^ up_ngb , cart_comm, &status);
MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, grid_info->p_id ^ up_ngb ,\
a0, 1, down_recv_subarray, down_ngb, grid_info->p_id ^ down_ngb, cart_comm, &status);
// MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, 10,\
// a0, 1, up_recv_subarray, up_ngb, 10, cart_comm, &status);
// MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, 11,\
// a0, 1, down_recv_subarray, down_ngb, 11, cart_comm, &status);
t_last = t_curr;
t_curr = MPI_Wtime();
*comm_time += t_curr - t_last;

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x] \
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x] \
+ ALPHA_NNZ * a0_local_Z[(y-1)*ldx+x-1] \
+ ALPHA_PNZ * a0_local_Z[(y-1)*ldx+x+1] \
+ ALPHA_NPZ * a0_local_Z[(y+1)*ldx+x-1] \
+ ALPHA_PPZ * a0_local_Z[(y+1)*ldx+x+1] \
+ ALPHA_NZN * a0_local_N[y*ldx+x-1] \
+ ALPHA_PZN * a0_local_N[y*ldx+x+1] \
+ ALPHA_NZP * a0_local_P[y*ldx+x-1] \
+ ALPHA_PZP * a0_local_P[y*ldx+x+1] \
+ ALPHA_ZNN * a0_local_N[(y-1)*ldx+x] \
+ ALPHA_ZPN * a0_local_N[(y+1)*ldx+x] \
+ ALPHA_ZNP * a0_local_P[(y-1)*ldx+x] \
+ ALPHA_ZPP * a0_local_P[(y+1)*ldx+x] \
+ ALPHA_NNN * a0_local_N[(y-1)*ldx+x-1] \
+ ALPHA_PNN * a0_local_N[(y-1)*ldx+x+1] \
+ ALPHA_NPN * a0_local_N[(y+1)*ldx+x-1] \
+ ALPHA_PPN * a0_local_N[(y+1)*ldx+x+1] \
+ ALPHA_NNP * a0_local_P[(y-1)*ldx+x-1] \
+ ALPHA_PNP * a0_local_P[(y-1)*ldx+x+1] \
+ ALPHA_NPP * a0_local_P[(y+1)*ldx+x-1] \
+ ALPHA_PPP * a0_local_P[(y+1)*ldx+x+1];
}// x
}// y
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z
}
}
t_last = t_curr;
t_curr = MPI_Wtime();
*calc_time += t_curr - t_last;
}
return buffer[nt % 2];
}

一维x轴划分

将x轴等距划分给n p npnp个进程,每个进程负责( n x / n p ) ∗ n y ∗ n z (nx/np)nynz(nx/np)∗ny∗nz的任务量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <mpi.h>
const char* version_name ="A mpi version with 1D partition in x";
#include "common.h"

#ifndef SET_Y_SIZE
#define SET_Y_SIZE 8
#endif
#ifndef SET_X_SIZE
#define SET_X_SIZE 256
#endif
#define MIN(a,b) ((a) < (b) ? (a) : (b))

MPI_Comm cart_comm;
int up_ngb, down_ngb;// y小的为down
MPI_Datatype up_send_subarray, up_recv_subarray;
MPI_Datatype down_send_subarray, down_recv_subarray;
MPI_Status status;

// #define useINDEX

// 创建分布式网格:可以根据7点或27点类型做不同的划分
void create_dist_grid(dist_grid_info_t *grid_info, int stencil_type) {
// 一维划分 沿x轴切
if(grid_info->global_size_x % grid_info->p_num != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_x, grid_info->p_num);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_x = grid_info->global_size_x / grid_info->p_num;
grid_info->local_size_y = grid_info->global_size_y;
grid_info->local_size_z = grid_info->global_size_z;

grid_info->offset_x = grid_info->local_size_x * grid_info->p_id;
grid_info->offset_y = 0;
grid_info->offset_z = 0;
grid_info->halo_size_x = 1;
grid_info->halo_size_y = 1;
grid_info->halo_size_z = 1;

// printf("pid: %d global: %d %d %d local : %d %d %d offset: %d %d %d\n", grid_info->p_id,\
// grid_info->global_size_z, grid_info->global_size_y, grid_info->global_size_x,\
// grid_info->local_size_z, grid_info->local_size_y, grid_info->local_size_x,\
// grid_info->offset_z, grid_info->offset_y, grid_info->offset_x);

// 创建通信的拓扑
int dims[1] = {grid_info->p_num};
int periods = 0;
MPI_Cart_create(MPI_COMM_WORLD, 1, dims, &periods, 0, &cart_comm);
MPI_Cart_shift(cart_comm, 0, 1, &down_ngb, &up_ngb);
// printf("pid: %d down: %d up: %d\n", grid_info->p_id, down_ngb, up_ngb);

// 创建subarray
int size[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->local_size_x + 2*grid_info->halo_size_x};
int subsize[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->halo_size_x};
int start[3];
// send to down_ngb
start[0] = 0; start[1] = 0; start[2] = grid_info->halo_size_x;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &down_send_subarray);
MPI_Type_commit(&down_send_subarray);
// printf("pid: %d down_send start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// recv from down_ngb
start[0] = 0; start[1] = 0; start[2] = 0;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &down_recv_subarray);
MPI_Type_commit(&down_recv_subarray);
// printf("pid: %d down_recv start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// send to up_ngb
start[0] = 0; start[1] = 0; start[2] = grid_info->local_size_x;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &up_send_subarray);
MPI_Type_commit(&up_send_subarray);
// printf("pid: %d up_send start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);

// recv from up_ngb
start[0] = 0; start[1] = 0; start[2] = grid_info->local_size_x + grid_info->halo_size_x;
MPI_Type_create_subarray(3, size, subsize, start, MPI_ORDER_C, DATA_TYPE, &up_recv_subarray);
MPI_Type_commit(&up_recv_subarray);
// printf("pid: %d up_recv start: %d %d %d\n", grid_info->p_id, start[0], start[1], start[2]);
}

void destroy_dist_grid(dist_grid_info_t *grid_info) {
for (int i = 1; i <= 8; i++) {
if (send_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&send_subarray[i]);
if (recv_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&recv_subarray[i]);
}
}

ptr_t stencil_7(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;

for(int t = 0; t < nt; ++t) {
cptr_t a0 = buffer[t % 2];
ptr_t a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, grid_info->p_id ^ down_ngb,\
a0, 1, up_recv_subarray, up_ngb, grid_info->p_id ^ up_ngb , cart_comm, &status);
MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, grid_info->p_id ^ up_ngb ,\
a0, 1, down_recv_subarray, down_ngb, grid_info->p_id ^ down_ngb, cart_comm, &status);
// MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, 10,\
// a0, 1, up_recv_subarray, up_ngb, 10, cart_comm, &status);
// MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, 11,\
// a0, 1, down_recv_subarray, down_ngb, 11, cart_comm, &status);


for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x]\
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x];

}// x loop
}// y loop
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z loop
}
}
}
return buffer[nt % 2];
}


ptr_t stencil_27(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
for(int t = 0; t < nt; ++t) {
cptr_t restrict a0 = buffer[t % 2];
ptr_t restrict a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, grid_info->p_id ^ down_ngb,\
a0, 1, up_recv_subarray, up_ngb, grid_info->p_id ^ up_ngb , cart_comm, &status);
MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, grid_info->p_id ^ up_ngb ,\
a0, 1, down_recv_subarray, down_ngb, grid_info->p_id ^ down_ngb, cart_comm, &status);
// MPI_Sendrecv(a0, 1, down_send_subarray, down_ngb, 10,\
// a0, 1, up_recv_subarray, up_ngb, 10, cart_comm, &status);
// MPI_Sendrecv(a0, 1, up_send_subarray, up_ngb, 11,\
// a0, 1, down_recv_subarray, down_ngb, 11, cart_comm, &status);

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x] \
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x] \
+ ALPHA_NNZ * a0_local_Z[(y-1)*ldx+x-1] \
+ ALPHA_PNZ * a0_local_Z[(y-1)*ldx+x+1] \
+ ALPHA_NPZ * a0_local_Z[(y+1)*ldx+x-1] \
+ ALPHA_PPZ * a0_local_Z[(y+1)*ldx+x+1] \
+ ALPHA_NZN * a0_local_N[y*ldx+x-1] \
+ ALPHA_PZN * a0_local_N[y*ldx+x+1] \
+ ALPHA_NZP * a0_local_P[y*ldx+x-1] \
+ ALPHA_PZP * a0_local_P[y*ldx+x+1] \
+ ALPHA_ZNN * a0_local_N[(y-1)*ldx+x] \
+ ALPHA_ZPN * a0_local_N[(y+1)*ldx+x] \
+ ALPHA_ZNP * a0_local_P[(y-1)*ldx+x] \
+ ALPHA_ZPP * a0_local_P[(y+1)*ldx+x] \
+ ALPHA_NNN * a0_local_N[(y-1)*ldx+x-1] \
+ ALPHA_PNN * a0_local_N[(y-1)*ldx+x+1] \
+ ALPHA_NPN * a0_local_N[(y+1)*ldx+x-1] \
+ ALPHA_PPN * a0_local_N[(y+1)*ldx+x+1] \
+ ALPHA_NNP * a0_local_P[(y-1)*ldx+x-1] \
+ ALPHA_PNP * a0_local_P[(y-1)*ldx+x+1] \
+ ALPHA_NPP * a0_local_P[(y+1)*ldx+x-1] \
+ ALPHA_PPP * a0_local_P[(y+1)*ldx+x+1];
}// x
}// y
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z
}
}
}
return buffer[nt % 2];
}

二维zy轴划分

综合上述一维划分的结果,在二维划分时考虑采用z和y轴联合划分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <mpi.h>
const char* version_name = "A mpi version with 2D partition in z & y";
#include "common.h"

#ifndef SET_Y_SIZE
#define SET_Y_SIZE 8
#endif
#ifndef SET_X_SIZE
#define SET_X_SIZE 256
#endif
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#define MAX(a,b) ((a) > (b) ? (a) : (b))

MPI_Comm cart_comm;
int cart_ids[2];
// 6 2 5
// y(1)
// ^ 3 0 1
// |
// | 7 4 8
// ------> z (0)
int oppo_idx[9] = {0, 3, 4, 1, 2, 7, 8, 5, 6};
int ngbs[9];
MPI_Datatype send_subarray[9], recv_subarray[9];
MPI_Status status;

// 创建分布式网格:可以根据7点或27点类型做不同的划分
void create_dist_grid(dist_grid_info_t *grid_info, int stencil_type) {
// 二维划分 沿zy轴切
int sqr_root = 1;
int num_proc_y, num_proc_z;
while (sqr_root*sqr_root < grid_info->p_num) sqr_root++;

if (sqr_root*sqr_root == grid_info->p_num) {
num_proc_z = num_proc_y = sqr_root;
} else {
int tmp = 1;
while (grid_info->p_num%tmp==0 && grid_info->p_num/tmp>tmp) tmp *= 2;
num_proc_z = grid_info->p_num / tmp;//跳出while时tmp>sqrt(p_num)
num_proc_y = tmp;
// num_proc_z = tmp;
// num_proc_y = grid_info->p_num / tmp;
}
if (grid_info->p_id == 0)
printf(" 2D partition: num_proc_y: %d, num_proc_z:%d\n", num_proc_y, num_proc_z);
// x轴不切
grid_info->local_size_x = grid_info->global_size_x;
// y轴
if (grid_info->global_size_y % num_proc_y != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_y, num_proc_y);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_y = grid_info->global_size_y / num_proc_y;
// z轴
if (grid_info->global_size_z % num_proc_z != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_z, num_proc_z);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_z = grid_info->global_size_z / num_proc_z;

// printf("pid: %d global: %d %d %d local : %d %d %d offset: %d %d %d\n", grid_info->p_id,\
// grid_info->global_size_z, grid_info->global_size_y, grid_info->global_size_x,\
// grid_info->local_size_z, grid_info->local_size_y, grid_info->local_size_x,\
// grid_info->offset_z, grid_info->offset_y, grid_info->offset_x);

// 创建通信的拓扑
ngbs[0] = grid_info->p_id;
int dims[2] = {num_proc_z, num_proc_y};
int periods[2] = {0, 0};
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, &periods, 0, &cart_comm);
MPI_Cart_shift(cart_comm, 0, 1, &ngbs[3], &ngbs[1]);
MPI_Cart_shift(cart_comm, 1, 1, &ngbs[4], &ngbs[2]);

int dist_y = 1;
if (ngbs[1]==MPI_PROC_NULL || ngbs[2]==MPI_PROC_NULL) ngbs[5] = MPI_PROC_NULL;
else ngbs[5] = ngbs[1] + dist_y;
if (ngbs[1]==MPI_PROC_NULL || ngbs[4]==MPI_PROC_NULL) ngbs[8] = MPI_PROC_NULL;
else ngbs[8] = ngbs[1] - dist_y;
if (ngbs[2]==MPI_PROC_NULL || ngbs[3]==MPI_PROC_NULL) ngbs[6] = MPI_PROC_NULL;
else ngbs[6] = ngbs[3] + dist_y;
if (ngbs[3]==MPI_PROC_NULL || ngbs[4]==MPI_PROC_NULL) ngbs[7] = MPI_PROC_NULL;
else ngbs[7] = ngbs[3] - dist_y;

MPI_Cart_coords(cart_comm, grid_info->p_id, 2, &cart_ids);

grid_info->offset_x = 0;
grid_info->offset_y = grid_info->local_size_y * cart_ids[1];
grid_info->offset_z = grid_info->local_size_z * cart_ids[0];
grid_info->halo_size_x = 1;
grid_info->halo_size_y = 1;
grid_info->halo_size_z = 1;

// printf("pid: %d cart_id[0]: %d cart_id[1]: %d\n %d %d %d %d %d %d %d %d %d\n offset_y:%d offset_z:%d\n", grid_info->p_id, cart_ids[0], cart_ids[1],\
// ngbs[0],ngbs[1],ngbs[2],ngbs[3],ngbs[4],ngbs[5],ngbs[6],ngbs[7],ngbs[8], grid_info->offset_y, grid_info->offset_z);


// 创建subarray
int size[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->local_size_x + 2*grid_info->halo_size_x};
int subsize[3], send_start[3], recv_start[3];
for (int i = 1; i <= 8; i++) {
switch (i) {
case 1:
subsize[0] = grid_info->halo_size_z;
subsize[1] = grid_info->local_size_y;// 注意是local_size 不带halo_width
break;
case 2:
subsize[0] = grid_info->local_size_z;
subsize[1] = grid_info->halo_size_y;
break;
case 3:
subsize[0] = grid_info->halo_size_z;
subsize[1] = grid_info->local_size_y;
break;
case 4:
subsize[0] = grid_info->local_size_z;
subsize[1] = grid_info->halo_size_y;
break;
default:// 5,6,7,8
subsize[0] = grid_info->halo_size_z;
subsize[1] = grid_info->halo_size_y;
break;
}
subsize[2] = grid_info->local_size_x + 2*grid_info->halo_size_x;

switch (i) {
case 1:// up_z
send_start[0] = grid_info->local_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = grid_info->local_size_z + grid_info->halo_size_z; recv_start[1] = grid_info->halo_size_y; recv_start[2] = 0;
break;
case 3:// down_z
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = 0; recv_start[1] = grid_info->halo_size_y; recv_start[2] = 0;
break;
case 2:// up_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->local_size_y; send_start[2] = 0;
recv_start[0] = grid_info->halo_size_z; recv_start[1] = grid_info->local_size_y + grid_info->halo_size_y; recv_start[2] = 0;
break;
case 4:// down_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = grid_info->halo_size_z; recv_start[1] = 0; recv_start[2] = 0;
break;
case 5:// up_z_up_y
send_start[0] = grid_info->local_size_z; send_start[1] = grid_info->local_size_y; send_start[2] = 0;
recv_start[0] = grid_info->local_size_z + grid_info->halo_size_z; recv_start[1] = grid_info->local_size_y + grid_info->halo_size_y; recv_start[2] = 0;
break;
case 6:// down_z_up_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->local_size_y; send_start[2] = 0;
recv_start[0] = 0; recv_start[1] = grid_info->local_size_y + grid_info->halo_size_y; recv_start[2] = 0;
break;
case 7:// down_z_down_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = 0; recv_start[1] = 0; recv_start[2] = 0;
break;
case 8:// up_z_down_y
send_start[0] = grid_info->local_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = grid_info->local_size_z + grid_info->halo_size_z; recv_start[1] = 0; recv_start[2] = 0;
break;
default:
break;
}
MPI_Type_create_subarray(3, size, subsize, send_start, MPI_ORDER_C, DATA_TYPE, &send_subarray[i]);
MPI_Type_commit(&send_subarray[i]);
MPI_Type_create_subarray(3, size, subsize, recv_start, MPI_ORDER_C, DATA_TYPE, &recv_subarray[i]);
MPI_Type_commit(&recv_subarray[i]);
}
}

void destroy_dist_grid(dist_grid_info_t *grid_info) {
for (int i = 1; i <= 8; i++) {
if (send_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&send_subarray[i]);
if (recv_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&recv_subarray[i]);
}
}

ptr_t stencil_7(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
double t_last, t_curr;

for(int t = 0; t < nt; ++t) {
cptr_t a0 = buffer[t % 2];
ptr_t a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
t_curr = MPI_Wtime();
for (int i = 1; i <= 4; i++) {// 7点stencil只需要通信4个
int oppo = oppo_idx[i];
MPI_Sendrecv(a0, 1, send_subarray[i] , ngbs[i] , grid_info->p_id ^ ngbs[i] ,\
a0, 1, recv_subarray[oppo], ngbs[oppo], grid_info->p_id ^ ngbs[oppo], cart_comm, &status);
}
t_last = t_curr;
t_curr = MPI_Wtime();
*comm_time += t_curr - t_last;

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x]\
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x];

}// x loop
}// y loop
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z loop
}
}
t_last = t_curr;
t_curr = MPI_Wtime();
*calc_time += t_curr - t_last;
}
return buffer[nt % 2];
}


ptr_t stencil_27(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
double t_last, t_curr;

for(int t = 0; t < nt; ++t) {
cptr_t restrict a0 = buffer[t % 2];
ptr_t restrict a1 = buffer[(t + 1) % 2];

// 通信同步(要让a0的边界是对的值!)
t_curr = MPI_Wtime();
for (int i = 1; i <= 8; i++) {// 27点stencil需要通信8个
int oppo = oppo_idx[i];
MPI_Sendrecv(a0, 1, send_subarray[i] , ngbs[i] , grid_info->p_id ^ ngbs[i] ,\
a0, 1, recv_subarray[oppo], ngbs[oppo], grid_info->p_id ^ ngbs[oppo], cart_comm, &status);
}
t_last = t_curr;
t_curr = MPI_Wtime();
*comm_time += t_curr - t_last;

t_last = MPI_Wtime();
for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x] \
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x] \
+ ALPHA_NNZ * a0_local_Z[(y-1)*ldx+x-1] \
+ ALPHA_PNZ * a0_local_Z[(y-1)*ldx+x+1] \
+ ALPHA_NPZ * a0_local_Z[(y+1)*ldx+x-1] \
+ ALPHA_PPZ * a0_local_Z[(y+1)*ldx+x+1] \
+ ALPHA_NZN * a0_local_N[y*ldx+x-1] \
+ ALPHA_PZN * a0_local_N[y*ldx+x+1] \
+ ALPHA_NZP * a0_local_P[y*ldx+x-1] \
+ ALPHA_PZP * a0_local_P[y*ldx+x+1] \
+ ALPHA_ZNN * a0_local_N[(y-1)*ldx+x] \
+ ALPHA_ZPN * a0_local_N[(y+1)*ldx+x] \
+ ALPHA_ZNP * a0_local_P[(y-1)*ldx+x] \
+ ALPHA_ZPP * a0_local_P[(y+1)*ldx+x] \
+ ALPHA_NNN * a0_local_N[(y-1)*ldx+x-1] \
+ ALPHA_PNN * a0_local_N[(y-1)*ldx+x+1] \
+ ALPHA_NPN * a0_local_N[(y+1)*ldx+x-1] \
+ ALPHA_PPN * a0_local_N[(y+1)*ldx+x+1] \
+ ALPHA_NNP * a0_local_P[(y-1)*ldx+x-1] \
+ ALPHA_PNP * a0_local_P[(y-1)*ldx+x+1] \
+ ALPHA_NPP * a0_local_P[(y+1)*ldx+x-1] \
+ ALPHA_PPP * a0_local_P[(y+1)*ldx+x+1];
}// x
}// y
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z
}
}
t_last = t_curr;
t_curr = MPI_Wtime();
*calc_time += t_curr - t_last;
}
return buffer[nt % 2];
}

计算通信重叠和非阻塞通信

基于上一节的二维zy轴划分,考虑计算通信重叠的实现,即每个进程先算自己的内halo区(邻居进程的外halo区),然后用非阻塞通信将内halo区数据通信。在此通信过程中,各进程计算自己真正的内部区域(不与其他进程有依赖关系的区域)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <mpi.h>
const char* version_name = "A mpi comp-comm overlapped";
#include "common.h"

#ifndef SET_Y_SIZE
#define SET_Y_SIZE 8
#endif
#ifndef SET_X_SIZE
#define SET_X_SIZE 256
#endif
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#define MAX(a,b) ((a) > (b) ? (a) : (b))

MPI_Comm cart_comm;
int cart_ids[2];
// 6 2 5
// y(1)
// ^ 3 0 1
// |
// | 7 4 8
// ------> z (0)
int oppo_idx[9] = {0, 3, 4, 1, 2, 7, 8, 5, 6};
int ngbs[9];
MPI_Datatype send_subarray[9], recv_subarray[9];

static int ih_z_beg[9], ih_z_end[9], ih_y_beg[9], ih_y_end[9];

// 创建分布式网格:可以根据7点或27点类型做不同的划分
void create_dist_grid(dist_grid_info_t *grid_info, int stencil_type) {
// 二维划分 沿zy轴切
int sqr_root = 1;
int num_proc_y, num_proc_z;
while (sqr_root*sqr_root < grid_info->p_num) sqr_root++;

if (sqr_root*sqr_root == grid_info->p_num) {
num_proc_z = num_proc_y = sqr_root;
} else {
int tmp = 1;
while (grid_info->p_num%tmp==0 && grid_info->p_num/tmp>tmp) tmp *= 2;
num_proc_z = grid_info->p_num / tmp;//跳出while时tmp>sqrt(p_num)
num_proc_y = tmp;
// num_proc_z = tmp;
// num_proc_y = grid_info->p_num / tmp;
}
if (grid_info->p_id == 0)
printf(" 2D partition: num_proc_y: %d, num_proc_z:%d\n", num_proc_y, num_proc_z);
// x轴不切
grid_info->local_size_x = grid_info->global_size_x;
// y轴
if (grid_info->global_size_y % num_proc_y != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_y, num_proc_y);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_y = grid_info->global_size_y / num_proc_y;
// z轴
if (grid_info->global_size_z % num_proc_z != 0) {
if (grid_info->p_id == 0)
printf(" Error: %d cannot divide %d!\n", grid_info->global_size_z, num_proc_z);
MPI_Abort(MPI_COMM_WORLD, 1);
}
grid_info->local_size_z = grid_info->global_size_z / num_proc_z;

// printf("pid: %d global: %d %d %d local : %d %d %d offset: %d %d %d\n", grid_info->p_id,\
// grid_info->global_size_z, grid_info->global_size_y, grid_info->global_size_x,\
// grid_info->local_size_z, grid_info->local_size_y, grid_info->local_size_x,\
// grid_info->offset_z, grid_info->offset_y, grid_info->offset_x);

// 创建通信的拓扑
ngbs[0] = grid_info->p_id;
int dims[2] = {num_proc_z, num_proc_y};
int periods[2] = {0, 0};
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, &periods, 0, &cart_comm);
MPI_Cart_shift(cart_comm, 0, 1, &ngbs[3], &ngbs[1]);
MPI_Cart_shift(cart_comm, 1, 1, &ngbs[4], &ngbs[2]);

int dist_y = 1;
if (ngbs[1]==MPI_PROC_NULL || ngbs[2]==MPI_PROC_NULL) ngbs[5] = MPI_PROC_NULL;
else ngbs[5] = ngbs[1] + dist_y;
if (ngbs[1]==MPI_PROC_NULL || ngbs[4]==MPI_PROC_NULL) ngbs[8] = MPI_PROC_NULL;
else ngbs[8] = ngbs[1] - dist_y;
if (ngbs[2]==MPI_PROC_NULL || ngbs[3]==MPI_PROC_NULL) ngbs[6] = MPI_PROC_NULL;
else ngbs[6] = ngbs[3] + dist_y;
if (ngbs[3]==MPI_PROC_NULL || ngbs[4]==MPI_PROC_NULL) ngbs[7] = MPI_PROC_NULL;
else ngbs[7] = ngbs[3] - dist_y;

MPI_Cart_coords(cart_comm, grid_info->p_id, 2, &cart_ids);

grid_info->offset_x = 0;
grid_info->offset_y = grid_info->local_size_y * cart_ids[1];
grid_info->offset_z = grid_info->local_size_z * cart_ids[0];
grid_info->halo_size_x = 1;
grid_info->halo_size_y = 1;
grid_info->halo_size_z = 1;

// printf("pid: %d cart_id[0]: %d cart_id[1]: %d\n %d %d %d %d %d %d %d %d %d\n offset_y:%d offset_z:%d\n", grid_info->p_id, cart_ids[0], cart_ids[1],\
// ngbs[0],ngbs[1],ngbs[2],ngbs[3],ngbs[4],ngbs[5],ngbs[6],ngbs[7],ngbs[8], grid_info->offset_y, grid_info->offset_z);


// 创建subarray
int size[3] = { grid_info->local_size_z + 2*grid_info->halo_size_z,\
grid_info->local_size_y + 2*grid_info->halo_size_y,\
grid_info->local_size_x + 2*grid_info->halo_size_x};
int subsize[3], send_start[3], recv_start[3];
for (int i = 1; i <= 8; i++) {
switch (i) {
case 1:
subsize[0] = grid_info->halo_size_z;
subsize[1] = grid_info->local_size_y;// 注意是local_size 不带halo_width
break;
case 2:
subsize[0] = grid_info->local_size_z;
subsize[1] = grid_info->halo_size_y;
break;
case 3:
subsize[0] = grid_info->halo_size_z;
subsize[1] = grid_info->local_size_y;
break;
case 4:
subsize[0] = grid_info->local_size_z;
subsize[1] = grid_info->halo_size_y;
break;
default:// 5,6,7,8
subsize[0] = grid_info->halo_size_z;
subsize[1] = grid_info->halo_size_y;
break;
}
subsize[2] = grid_info->local_size_x + 2*grid_info->halo_size_x;

switch (i) {
case 1:// up_z
send_start[0] = grid_info->local_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = grid_info->local_size_z + grid_info->halo_size_z; recv_start[1] = grid_info->halo_size_y; recv_start[2] = 0;
break;
case 3:// down_z
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = 0; recv_start[1] = grid_info->halo_size_y; recv_start[2] = 0;
break;
case 2:// up_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->local_size_y; send_start[2] = 0;
recv_start[0] = grid_info->halo_size_z; recv_start[1] = grid_info->local_size_y + grid_info->halo_size_y; recv_start[2] = 0;
break;
case 4:// down_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = grid_info->halo_size_z; recv_start[1] = 0; recv_start[2] = 0;
break;
case 5:// up_z_up_y
send_start[0] = grid_info->local_size_z; send_start[1] = grid_info->local_size_y; send_start[2] = 0;
recv_start[0] = grid_info->local_size_z + grid_info->halo_size_z; recv_start[1] = grid_info->local_size_y + grid_info->halo_size_y; recv_start[2] = 0;
break;
case 6:// down_z_up_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->local_size_y; send_start[2] = 0;
recv_start[0] = 0; recv_start[1] = grid_info->local_size_y + grid_info->halo_size_y; recv_start[2] = 0;
break;
case 7:// down_z_down_y
send_start[0] = grid_info->halo_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = 0; recv_start[1] = 0; recv_start[2] = 0;
break;
case 8:// up_z_down_y
send_start[0] = grid_info->local_size_z; send_start[1] = grid_info->halo_size_y; send_start[2] = 0;
recv_start[0] = grid_info->local_size_z + grid_info->halo_size_z; recv_start[1] = 0; recv_start[2] = 0;
break;
default:
break;
}
MPI_Type_create_subarray(3, size, subsize, send_start, MPI_ORDER_C, DATA_TYPE, &send_subarray[i]);
MPI_Type_commit(&send_subarray[i]);
MPI_Type_create_subarray(3, size, subsize, recv_start, MPI_ORDER_C, DATA_TYPE, &recv_subarray[i]);
MPI_Type_commit(&recv_subarray[i]);
}

// 记录计算通信重叠部分的内halo区(注意这是内halo!)
for (int dir = 1; dir <= 4; dir++) {
//
// ----------------------------
// | 2 | |
// |--------------------| |
// | | | |
// | | | 1 |
// | | | |
// y | 3 | | |
// ^ | | | |
// | | |--------------------|
// | | | 4 |
// | ----------------------------
// O----> z
switch (dir) {
case 1:
ih_z_beg[dir] = grid_info->local_size_z;
ih_z_end[dir] = grid_info->halo_size_z + grid_info->local_size_z;
ih_y_beg[dir] = grid_info->halo_size_y * 2;
ih_y_end[dir] = grid_info->halo_size_y + grid_info->local_size_y;
break;
case 2:
ih_z_beg[dir] = grid_info->halo_size_z;
ih_z_end[dir] = grid_info->local_size_z;
ih_y_beg[dir] = grid_info->local_size_y;
ih_y_end[dir] = ih_y_beg[dir] + grid_info->halo_size_y;
break;
case 3:
ih_z_beg[dir] = grid_info->halo_size_z;
ih_z_end[dir] = grid_info->halo_size_z * 2;
ih_y_beg[dir] = grid_info->halo_size_y;
ih_y_end[dir] = grid_info->local_size_y;
break;
case 4:
ih_z_beg[dir] = grid_info->halo_size_z * 2;
ih_z_end[dir] = grid_info->halo_size_z + grid_info->local_size_z;
ih_y_beg[dir] = grid_info->halo_size_y;
ih_y_end[dir] = grid_info->halo_size_y * 2;
break;
default:
break;
}
// printf("pid %d, dir %d, %d %d %d %d\n", grid_info->p_id, dir, ih_z_beg[dir], ih_z_end[dir], ih_y_beg[dir], ih_y_end[dir]);
}

}

void destroy_dist_grid(dist_grid_info_t *grid_info) {
for (int i = 1; i <= 8; i++) {
if (send_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&send_subarray[i]);
if (recv_subarray[i] != MPI_DATATYPE_NULL)
MPI_Type_free(&recv_subarray[i]);
}
}

ptr_t stencil_7(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
MPI_Request req_send[4], req_recv[4];
MPI_Status status[4];

// for boundary conditions
for (int i = 1; i <= 4; i++) {// 7点stencil只需要通信4个
int oppo = oppo_idx[i];
MPI_Sendrecv(buffer[0], 1, send_subarray[i] , ngbs[i] , grid_info->p_id ^ ngbs[i] ,\
buffer[0], 1, recv_subarray[oppo], ngbs[oppo], grid_info->p_id ^ ngbs[oppo], cart_comm, &status[i-1]);
}

// y_start += grid_info->halo_size_y; y_end -= grid_info->halo_size_y;
// z_start += grid_info->halo_size_z; z_end -= grid_info->halo_size_z;

for(int t = 0; t < nt; ++t) {
cptr_t a0 = buffer[t % 2];
ptr_t a1 = buffer[(t + 1) % 2];

// 先计算内halo区
for (int dir = 1; dir <= 4; dir++) {
ptr_t a1_local = a1 + ih_z_beg[dir]*ldx*ldy + ih_y_beg[dir]*ldx;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;
int yy_end = ih_y_end[dir]-ih_y_beg[dir];
for (int z = ih_z_beg[dir]; z < ih_z_end[dir]; z++){
for (int y = 0; y < yy_end; y++)
for (int x = x_start; x < x_end; x++)
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x]\
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x];
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}
}
// 隐藏非阻塞通信
for (int dir = 1; dir <= 4; dir++) {
int oppo = oppo_idx[dir];
MPI_Isend(a1, 1, send_subarray[dir], ngbs[dir], grid_info->p_id ^ ngbs[dir], cart_comm, &req_send[dir-1]);
MPI_Irecv(a1, 1, recv_subarray[oppo], ngbs[oppo], grid_info->p_id ^ ngbs[oppo], cart_comm, &req_recv[oppo-1]);
}

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x]\
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x];

}// x loop
}// y loop
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z loop
}
}

MPI_Waitall(4, req_recv, status);
MPI_Waitall(4, req_send, status);
}

return buffer[nt % 2];
}


ptr_t stencil_27(ptr_t grid, ptr_t aux, const dist_grid_info_t *grid_info, int nt, double * calc_time, double * comm_time) {
ptr_t buffer[2] = {grid, aux};
int x_start = grid_info->halo_size_x, x_end = grid_info->local_size_x + grid_info->halo_size_x;
int y_start = grid_info->halo_size_y, y_end = grid_info->local_size_y + grid_info->halo_size_y;
int z_start = grid_info->halo_size_z, z_end = grid_info->local_size_z + grid_info->halo_size_z;
int ldx = grid_info->local_size_x + 2 * grid_info->halo_size_x;
int ldy = grid_info->local_size_y + 2 * grid_info->halo_size_y;
int ldz = grid_info->local_size_z + 2 * grid_info->halo_size_z;
MPI_Request req_send[8], req_recv[8];
MPI_Status status[8];

// for boundary conditions
for (int i = 1; i <= 8; i++) {// 27点stencil需要通信8个
int oppo = oppo_idx[i];
MPI_Sendrecv(buffer[0], 1, send_subarray[i] , ngbs[i] , grid_info->p_id ^ ngbs[i] ,\
buffer[0], 1, recv_subarray[oppo], ngbs[oppo], grid_info->p_id ^ ngbs[oppo], cart_comm, &status[i-1]);
}

// 刨去外周一圈,导致对不齐(或者别的原因?),速度骤降,比重新再多算一遍外周还慢得多!!!
// y_start += grid_info->halo_size_y; y_end -= grid_info->halo_size_y;
// z_start += grid_info->halo_size_z; z_end -= grid_info->halo_size_z;

for(int t = 0; t < nt; ++t) {
cptr_t a0 = buffer[t % 2];
ptr_t a1 = buffer[(t + 1) % 2];

// 先计算内halo区
for (int dir = 1; dir <= 4; dir++) {
ptr_t a1_local = a1 + ih_z_beg[dir]*ldx*ldy + ih_y_beg[dir]*ldx;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;
int yy_end = ih_y_end[dir]-ih_y_beg[dir];

for (int z = ih_z_beg[dir]; z < ih_z_end[dir]; z++){
for (int y = 0; y < yy_end; y++)
for (int x = x_start; x < x_end; x++)
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x] \
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x] \
+ ALPHA_NNZ * a0_local_Z[(y-1)*ldx+x-1] \
+ ALPHA_PNZ * a0_local_Z[(y-1)*ldx+x+1] \
+ ALPHA_NPZ * a0_local_Z[(y+1)*ldx+x-1] \
+ ALPHA_PPZ * a0_local_Z[(y+1)*ldx+x+1] \
+ ALPHA_NZN * a0_local_N[y*ldx+x-1] \
+ ALPHA_PZN * a0_local_N[y*ldx+x+1] \
+ ALPHA_NZP * a0_local_P[y*ldx+x-1] \
+ ALPHA_PZP * a0_local_P[y*ldx+x+1] \
+ ALPHA_ZNN * a0_local_N[(y-1)*ldx+x] \
+ ALPHA_ZPN * a0_local_N[(y+1)*ldx+x] \
+ ALPHA_ZNP * a0_local_P[(y-1)*ldx+x] \
+ ALPHA_ZPP * a0_local_P[(y+1)*ldx+x] \
+ ALPHA_NNN * a0_local_N[(y-1)*ldx+x-1] \
+ ALPHA_PNN * a0_local_N[(y-1)*ldx+x+1] \
+ ALPHA_NPN * a0_local_N[(y+1)*ldx+x-1] \
+ ALPHA_PPN * a0_local_N[(y+1)*ldx+x+1] \
+ ALPHA_NNP * a0_local_P[(y-1)*ldx+x-1] \
+ ALPHA_PNP * a0_local_P[(y-1)*ldx+x+1] \
+ ALPHA_NPP * a0_local_P[(y+1)*ldx+x-1] \
+ ALPHA_PPP * a0_local_P[(y+1)*ldx+x+1];
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}
}
// 隐藏非阻塞通信
for (int dir = 1; dir <= 8; dir++) {
int oppo = oppo_idx[dir];
MPI_Isend(a1, 1, send_subarray[dir], ngbs[dir], grid_info->p_id ^ ngbs[dir], cart_comm, &req_send[dir-1]);
MPI_Irecv(a1, 1, recv_subarray[oppo], ngbs[oppo], grid_info->p_id ^ ngbs[oppo], cart_comm, &req_recv[oppo-1]);
}

for (int JJ = y_start; JJ < y_end; JJ += SET_Y_SIZE) {
for (int II = x_start; II < x_end; II += SET_X_SIZE){
int REAL_Y_SIZE = MIN(SET_Y_SIZE, y_end - JJ);
int REAL_X_SIZE = MIN(SET_X_SIZE, x_end - II);

ptr_t a1_local = a1 + z_start*ldx*ldy + JJ*ldx + II;
cptr_t a0_local_Z = a0 + (a1_local - a1);
cptr_t a0_local_P = a0_local_Z + ldy*ldx;
cptr_t a0_local_N = a0_local_Z - ldy*ldx;

for(int z = z_start; z < z_end; ++z) {
for(int y = 0; y < REAL_Y_SIZE; y++) {
#pragma unroll
for(int x = 0; x < REAL_X_SIZE; x++) {
a1_local[y*ldx+x] \
= ALPHA_ZZZ * a0_local_Z[y*ldx+x] \
+ ALPHA_NZZ * a0_local_Z[y*ldx+x-1] \
+ ALPHA_PZZ * a0_local_Z[y*ldx+x+1] \
+ ALPHA_ZNZ * a0_local_Z[(y-1)*ldx+x] \
+ ALPHA_ZPZ * a0_local_Z[(y+1)*ldx+x] \
+ ALPHA_ZZN * a0_local_N[y*ldx+x] \
+ ALPHA_ZZP * a0_local_P[y*ldx+x] \
+ ALPHA_NNZ * a0_local_Z[(y-1)*ldx+x-1] \
+ ALPHA_PNZ * a0_local_Z[(y-1)*ldx+x+1] \
+ ALPHA_NPZ * a0_local_Z[(y+1)*ldx+x-1] \
+ ALPHA_PPZ * a0_local_Z[(y+1)*ldx+x+1] \
+ ALPHA_NZN * a0_local_N[y*ldx+x-1] \
+ ALPHA_PZN * a0_local_N[y*ldx+x+1] \
+ ALPHA_NZP * a0_local_P[y*ldx+x-1] \
+ ALPHA_PZP * a0_local_P[y*ldx+x+1] \
+ ALPHA_ZNN * a0_local_N[(y-1)*ldx+x] \
+ ALPHA_ZPN * a0_local_N[(y+1)*ldx+x] \
+ ALPHA_ZNP * a0_local_P[(y-1)*ldx+x] \
+ ALPHA_ZPP * a0_local_P[(y+1)*ldx+x] \
+ ALPHA_NNN * a0_local_N[(y-1)*ldx+x-1] \
+ ALPHA_PNN * a0_local_N[(y-1)*ldx+x+1] \
+ ALPHA_NPN * a0_local_N[(y+1)*ldx+x-1] \
+ ALPHA_PPN * a0_local_N[(y+1)*ldx+x+1] \
+ ALPHA_NNP * a0_local_P[(y-1)*ldx+x-1] \
+ ALPHA_PNP * a0_local_P[(y-1)*ldx+x+1] \
+ ALPHA_NPP * a0_local_P[(y+1)*ldx+x-1] \
+ ALPHA_PPP * a0_local_P[(y+1)*ldx+x+1];
}// x
}// y
a1_local = a1_local + ldx*ldy;
a0_local_N = a0_local_Z;
a0_local_Z = a0_local_P;
a0_local_P = a0_local_P + ldx*ldy;
}// z
}
}

MPI_Waitall(8, req_recv, status);
MPI_Waitall(8, req_send, status);
}
return buffer[nt % 2];
}

值得一提的是,当使用8个节点(1024核)时,会出现执行程序非常慢,甚至有时提交任务太久没执行完而被作业系统杀掉的情况。但执行后输出的结果却显示时间仍然只是零点几秒,这大概是由于MPI-IO读取数据时非常耗时。具体原因我没有深究,但由于等待时间实在太久,所以只进行了test.sh中的测试,即跑了16个时间步的循环。而跨节点时本来性能就会有较大波动。

实际上,应用计算通信重叠会导致在某些并行度下性能有较明显的下降。这可能是因为刨去内halo区剩下的区域并不能对齐,导致后续在计算真正的内部区域时,会有更长的计算时间。所以在此只是尝试了一下,后续的优化没有应用计算通信重叠。

节点内进程映射优化

经过进程映射的优化(进程尽可能均匀散布于整个节点,核与核之间距离尽可能远)可以得到单节点内(进程数较小时)更高的可扩展性!这有两个原因。

一方面是L1和L2 cache是各个cpu独有的,而L3 cache整个numa-region内的32个核共享。MPI程序是分离的地址空间,一个进程计算时所需访存的地址肯定与别的进程不一样,不怕伪共享,反而是多个进程共用一个numa-region内的核时会导致L3 cache共用而产生的capacity miss或conflict miss增多!所以应尽量让进程分布距离远一些,避免过度聚集而致共享的L3 cache过热(在进程数较少时可以独享或尽可能多占L3 cache),使整个节点的负载均衡。

另一方面,我认为更重要的是,内存总线一般是几个核共用一条的(具体的排线方式不同机器有差异,只是一般情况),比如在该节点128核内,0-3核(核组0),4-7核(核组1)等是以核组为单位共享内存总线的。所以当进程分布得更散落时,有利于提高机器的内存带宽利用率。这对于stencil这种memory-bounded、严重吃带宽的程序而言,应该是相比于cache更重要的因素。

该映射优化可以通过计算给定进程数np时均匀分布于整个节点的步长stride,和mpirun的命令行参数--map-by slot:PE=$stride --bind-to core来实现,可见mpi-benchmark.sh文件和mpi-test.sh文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
#SBATCH --nodes=1
#BATCH --ntasks-per-node=1

if [ "$#" -ne 3 ]; then
echo "Usage: $0 <executable> <number of nodes> <number of processes per node> " >&2
exit 1
fi

export DAPL_DBG_TYPE=0
stride=$[128 / $3]

DATAPATH=/storage/readonly/stencil_data

salloc -N $2 --exclusive --ntasks-per-node $3 mpirun --map-by slot:PE=$stride --bind-to core --report-bindings $1 7 256 256 256 16 ${DATAPATH}/stencil_data_256x256x256 ${DATAPATH}/stencil_answer_7_256x256x256_16steps
salloc -N $2 --exclusive --ntasks-per-node $3 mpirun --map-by slot:PE=$stride --bind-to core --report-bindings $1 7 384 384 384 16 ${DATAPATH}/stencil_data_384x384x384 ${DATAPATH}/stencil_answer_7_384x384x384_16steps
salloc -N $2 --exclusive --ntasks-per-node $3 mpirun --map-by slot:PE=$stride --bind-to core --report-bindings $1 7 512 512 512 16 ${DATAPATH}/stencil_data_512x512x512 ${DATAPATH}/stencil_answer_7_512x512x512_16steps
salloc -N $2 --exclusive --ntasks-per-node $3 mpirun --map-by slot:PE=$stride --bind-to core --report-bindings $1 27 256 256 256 16 ${DATAPATH}/stencil_data_256x256x256 ${DATAPATH}/stencil_answer_27_256x256x256_16steps
salloc -N $2 --exclusive --ntasks-per-node $3 mpirun --map-by slot:PE=$stride --bind-to core --report-bindings $1 27 384 384 384 16 ${DATAPATH}/stencil_data_384x384x384 ${DATAPATH}/stencil_answer_27_384x384x384_16steps
salloc -N $2 --exclusive --ntasks-per-node $3 mpirun --map-by slot:PE=$stride --bind-to core --report-bindings $1 27 512 512 512 16 ${DATAPATH}/stencil_data_512x512x512 ${DATAPATH}/stencil_answer_27_512x512x512_16steps

LINUX网络编程基础知识

TCP/IP协议概述

协议protocol:通信双方必须遵循的规矩 由iso规定,

osi参考模型:(应-表-会-传-网-数-物)

  • 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层

tcp/ip模型4层:

  • 应用层{http超文本传输协议 ftp文件传输协议 telnet远程登录 ssh安全外壳协议 stmp简单邮件发送 pop3收邮件}
  • 传输层{tcp传输控制协议,udp用户数据包协议}
  • 网络层{ip网际互联协议 icmp网络控制消息协议 igmp网络组管理协议}
  • 链路层{arp地址转换协议,rarp反向地址转换协议,mpls多协议标签交换}

TCP协议:传输控制协议 面向连接的协议 能保证传输安全可靠 速度慢(有3次握手)

UDP协议:用户数据包协议 非面向连接 速度快 不可靠

通常是ip地址后面跟上端口号:ip用来定位主机 port区别应用(进程)

http的端口号80 ssh—>22 telnet—>23 ftp—>21 用户自己定义的通常要大于1024

OSI参考模型及TCP/IP参考模型

TCP/IP协议族的每一层的作用:

  • 链路层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。
  • 网络层:负责将数据帧封装成IP数据报,并运行必要的路由算法。
  • 传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。
  • 应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。

TCP/IP协议族的每一层协议的相关注解:

  • ARP:(地址转换协议)用于获得同一物理网络中的硬件主机地址。
  • MPLS:(多协议标签交换)很有发展前景的下一代网络协议。
  • IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。
  • ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。
  • IGMP:(网络组管理协议)被IP主机用来向本地多路广播路由器报告主机组成员的协议。
  • TCP:(传输控制协议)为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到相应的应用程序。
  • UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。

TCP协议

概述

TCP是TCP/IP体系中面向连接的运输层协议,它提供全双工和可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。

首先,TCP要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。

其次,TCP采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。

最后,采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP采用可变长的滑动窗口,使得发送端与接收端可根据自己的CPU和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。

三次握手协议

在利用TCP实现源主机和目的主机通信时,目的主机必须同意,否则TCP连接无法建立。为了确保TCP连接的成功建立,TCP采用了一种称为三次握手的方式,三次握手方式使得“序号/确认号”系统能够正常工作,从而使它们的序号达成同步。如果三次握手成功,则连接建立成功,可以开始传送数据信息。

其三次握手分别为:

  1. 源主机A的TCP向主机B发送连接请求报文段,其首部中的SYN(同步)标志位应置为1,表示想跟目标主机B建立连接,进行通信,并发送一个同步序列号X(例:SEQ=100)进行同步,表明在后面传送数据时的第一个数据字节的序号为X+1(即101)。
  2. 目标主机B的TCP收到连接请求报文段后,如同意,则发回确认。再确认报中应将ACK位和SYN位置为1.确认号为X+1,同时也为自己选择一个序号Y。
  3. 源主机A的TCP收到目标主机B的确认后要想目标主机B给出确认。其ACK置为1,确认号为Y+1,而自己的序号为X+1。TCP的标准规定,SYN置1的报文段要消耗掉一个序号。

运行客户进程的源主机A的TCP通知上层应用进程,连接已经建立。当源主机A向目标主机B发送第一个数据报文段时,其序号仍为X+1,因为前一个确认报文段并不消耗序号。

当运行服务进程的目标主机B的TCP收到源主机A的确认后,也通知其上层应用进程,连接已经建立。至此建立了一个全双工的连接。

三次握手:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。

TCP数据报头

TCP头信息

  • 源端口、目的端口:16位长。标识出远端和本地的端口号。
  • 序号:32位长。标识发送的数据报的顺序。
  • 确认号:32位长。希望收到的下一个数据报的序列号。
  • TCP头长:4位长。表明TCP头中包含多少个32位字。
  • 6位未用。
  • ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据报不包含确认信息,确认字段被省略。
  • PSH:表示是带有PUSH标志的数据。接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才发送。
  • RST:用于复位由于主机崩溃或其他原因而出现的错误的连接。还可以用于拒绝非法的数据报或拒绝连接请求。
  • SYN:用于建立连接。
  • FIN:用于释放连接。
  • 窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
  • 校验和:16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。
  • 可选项:0个或多个32位字。包括最大TCP载荷,窗口比例、选择重复数据报等选项。

UDP协议

概述

UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。

它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。

协议的选择

对数据可靠性的要求

对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送。

应用的实时性

TCP协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。

网络的可靠性

由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷。

网络相关概念

套接口的概念:

套接口,也叫“套接字”。是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户。它是网络进程的ID。网络通信,归根到底还是进程间的通信(不同计算机上的进程间通信)。在网络中,每一个节点(计算机或路由)都有一个网络地址,也就是IP地址。两个进程通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,也就是端口号(PORT)。在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程之间是一一对应关系。所以,使用端口号和网络地址的组合可以唯一的确定整个网络中的一个网络进程。

例如,如网络中某一台计算机的IP为10.92.20.160,操作系统分配给计算机中某一应用程序进程的端口号为1500,则此时 10.92.20.160 1500就构成了一个套接口。

端口号的概念:

在网络技术中,端口大致有两种意思:一是物理意义上的端口,如集线器、交换机、路由器等用于连接其他网络设备的接口。二是指TCP/IP协议中的端口,端口号的范围从0~65535,一类是由互联网指派名字和号码公司ICANN负责分配给一些常用的应用程序固定使用的“周知的端口”,其值一般为0~1023.例如http的端口号是80,ftp为21,ssh为22,telnet为23等。还有一类是用户自己定义的,通常是大于1024的整型值。

ip地址的表示:

通常用户在表达IP地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在通常使用的socket编程中使用的则是二进制值,这就需要将这两个数值进行转换。

ipv4地址:32bit, 4字节,通常采用点分十进制记法。

例如对于:10000000 00001011 00000011 00011111

点分十进制表示为:128.11.3.31

socket概念

Linux中的网络编程是通过socket接口来进行的。socket是一种特殊的I/O接口,它也是一种文件描述符。它是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。

每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的;

socket类型

(1)流式socket(SOCK_STREAM) 用于TCP通信

流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。

(2)数据报socket(SOCK_DGRAM) 用于UDP通信

数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。

(3)原始socket (SOCK_RAW) 用于新的网络协议实现的测试等

原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

socket信息数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/
};

struct sockaddr_in
{
short int sa_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};

struct in_addr
{
unsigned long int s_addr; /* 32位IPv4地址,网络字节序 */
};

头文件

数据存储优先顺序的转换

计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式)。内存的低地址存储数据的低字节,高地址存储数据的高字节的方式叫小端模式。内存的高地址存储数据的低字节,低地址存储数据高字节的方式称为大端模式。

eg:对于内存中存放的数0x12345678来说

  • 如果是采用大端模式存放的,则其真实的数是:0x12345678
  • 如果是采用小端模式存放的,则其真实的数是:0x78563412

如果称某个系统所采用的字节序为主机字节序,则它可能是小端模式的,也可能是大端模式的。而端口号和IP地址都是以网络字节序存储的,不是主机字节序,网络字节序都是大端模式。要把主机字节序和网络字节序相互对应起来,需要对这两个字节存储优先顺序进行相互转化。这里用到四个函数:htons(),ntohs(),htonl()和ntohl().这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。

地址格式转化

通常用户在表达地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在通常使用的socket编程中使用的则是32位的网络字节序的二进制值,这就需要将这两个数值进行转换。这里在Ipv4中用到的函数有inet_aton()inet_addr()inet_ntoa(),而IPV4和Ipv6兼容的函数有inet_pton()inet_ntop()

IPv4的函数原型:

1
2
3
4
5
6
7
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *straddr, struct in_addr *addrptr);
char *inet_ntoa(struct in_addr inaddr);
in_addr_t inet_addr(const char *straddr);

函数inet_aton():将点分十进制数的IP地址转换成为网络字节序的32位二进制数值。返回值:成功,则返回1,不成功返回0.

- 参数straddr:存放输入的点分十进制数IP地址字符串。
- 参数addrptr:传出参数,保存网络字节序的32位二进制数值。

函数inet_ntoa():将网络字节序的32位二进制数值转换为点分十进制的IP地址。

函数inet_addr():功能与inet_aton相同,但是结果传递的方式不同。inet_addr()若成功则返回32位二进制的网络字节序地址。

IPv4和IPv6的函数原型:

1
2
3
4
#include <arpa/inet.h>

int inet_pton(int family, const char *src, void *dst);
const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);

函数inet_pton跟inet_aton实现的功能类似,只是多了family参数,该参数指定为AF_INET,表示是IPv4协议,如果是AF_INET6,表示IPv6协议。

函数inet_ntop跟inet_ntoa类似,其中len表示表示转换之后的长度(字符串的长度)。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
char ip[] = "192.168.0.101";

struct in_addr myaddr;
/* inet_aton */
int iRet = inet_aton(ip, &myaddr);
printf("%x\n", myaddr.s_addr);

/* inet_addr */
printf("%x\n", inet_addr(ip));

/* inet_pton */
iRet = inet_pton(AF_INET, ip, &myaddr);
printf("%x\n", myaddr.s_addr);

myaddr.s_addr = 0xac100ac4;
/* inet_ntoa */
printf("%s\n", inet_ntoa(myaddr));

/* inet_ntop */
inet_ntop(AF_INET, &myaddr, ip, 16);
puts(ip);
return 0;
}

名字地址转化

通常,人们在使用过程中都不愿意记忆冗长的IP地址,尤其到Ipv6时,地址长度多达128位,那时就更加不可能一次性记忆那么长的IP地址了。因此,使用主机名或域名将会是很好的选择。主机名与域名的区别:主机名通常在局域网里面使用,通过/etc/hosts文件,主机名可以解析到对应的ip;域名通常是再internet上使用。

众所周知,百度的域名为:www.baidu.com,而这个域名其实对应了一个百度公司的IP地址,那么百度公司的IP地址是多少呢?我们可以利用ping www.baidu.com来得到百度公司的ip地址,如图。那么,系统是如何将www.baidu.com 这个域名转化为IP地址220.181.111.148的呢?

在linux中,有一些函数可以实现主机名和地址的转化,最常见的有gethostbyname()gethostbyaddr()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()则是逆操作,是将IP地址转化为主机名。

函数原型:

1
2
3
#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);

结构体:

1
2
3
4
5
6
7
8
9
10
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*主机IP地址类型 IPv4为AF_INET*/
int h_length; /*主机IP地址字节长度,对于IPv4是4字节,即32位*/
char **h_addr_list; /*主机的IP地址列表*/
}

#define h_addr h_addr_list[0] /*保存的是ip地址*/

函数gethostbyname():用于将域名(www.baidu.com)或主机名转换为IP地址。参数hostname指向存放域名或主机名的字符串。

函数gethostbyaddr():用于将IP地址转换为域名或主机名。参数addr是一个IP地址,此时这个ip地址不是普通的字符串,而是要通过函数inet_aton()转换。len为IP地址的长度,AF_INET为4。family可用AF_INET:Ipv4或AF_INET6:Ipv6。

Example1:将百度的www.baidu.com 转换为ip地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <netdb.h>
#include <sys/socket.h>
#include <stdio.h>

int main(int argc, char **argv)
{
char *ptr, **pptr;
struct hostent *hptr;
char str[32] = {'\0'};
/* 取得命令后第一个参数,即要解析的域名或主机名 */
ptr = argv[1]; //如www.baidu.com
/* 调用gethostbyname()。结果存在hptr结构中 */
if((hptr = gethostbyname(ptr)) == NULL)
{
printf(" gethostbyname error for host:%s\n", ptr);
return 0;
}
/* 将主机的规范名打出来 */
printf("official hostname:%s\n", hptr->h_name);
/* 主机可能有多个别名,将所有别名分别打出来 */
for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)
printf(" alias:%s\n", *pptr);
/* 根据地址类型,将地址打出来 */
switch(hptr->h_addrtype)
{
case AF_INET:
case AF_INET6:
pptr = hptr->h_addr_list;
/* 将刚才得到的所有地址都打出来。其中调用了inet_ntop()函数 */
for(; *pptr!=NULL; pptr++)
printf(" address:%s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
printf(" first address: %s\n", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
break;
default:
printf("unknown address type\n");
break;
}
return 0;
}

编译运行

1
2
3
4
5
6
7
8
9
10
11
12
13
#gcc test.c

#./a.out www.baidu.com

official hostname:www.a.shifen.com

alias:www.baidu.com

address: 220.181.111.148

……

first address: 220.181.111.148

socket编程

使用TCP协议的流程图

TCP通信的基本步骤如下:

服务端:socket—-bind—-listen—-while(1){—-accept—-recv—-send—-close—-}—-close

客户端:socket——————————————connect—-send—-recv————————-close

服务器端

头文件包含:

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

socket函数

生成一个套接口描述符。

原型:int socket(int domain,int type,int protocol);

  • domain { AF_INET:Ipv4网络协议 AF_INET6:IPv6网络协议}
  • type { tcp:SOCK_STREAM udp:SOCK_DGRAM}
  • protocol 指定socket所使用的传输协议编号。通常为0.

返回值:成功则返回套接口描述符,失败返回-1。

常用实例:

1
2
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){perror("socket");exit(-1);}

bind函数

用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。

原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);

参数:

  • sockfd 为前面socket的返回值。
  • my_addr 为结构体指针变量

对于不同的socket domain定义了一个通用的数据结构

1
2
3
4
5
struct sockaddr  //此结构体不常用 
{
unsigned short int sa_family; //调用socket()时的domain参数,即AF_INET值。
char sa_data[14]; //最多使用14个字符长度
};

此sockaddr结构会因使用不同的socket domain而有不同结构定义, 例如使用AF_INET domain,其socketaddr结构定义便为

1
2
3
4
5
6
7
8
9
struct sockaddr_in  //常用的结构体
{
unsigned short int sin_family; //即为sa_family AF_INET
uint16_t sin_port; //为使用的port编号
struct in_addr sin_addr; //为IP 地址
unsigned char sin_zero[8]; //未使用
};

struct in_addr { uint32_t s_addr; }; // addrlen sockaddr的结构体长度。通常是计算sizeof(struct sockaddr);

返回值:成功则返回0,失败返回-1

常用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct sockaddr_in my_addr;  //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空

//或bzero(&my_addr, sizeof(struct sockaddr));

my_addr.sin_family = AF_INET; //表示采用Ipv4网络协议
my_addr.sin_port = htons(8888); //表示端口号为8888,通常是大于1024的一个值。

//htons()用来将参数指定的16位hostshort转换成网络字符顺序

my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); // inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为INADDR_ANY,这表示服务器自动填充本机IP地址。

if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1) {
perror("bind");
close(sfd);
exit(-1);
}

(注:通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。)

listen函数

使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。

原型:int listen(int sockfd, int backlog);

参数:

  • sockfd 为前面socket的返回值.即sfd
  • backlog 指定同时能处理的最大连接要求,通常为10或者5。 最大值可设至128

返回值:成功则返回0,失败返回-1

常用实例:

1
2
if(listen(sfd, 10) == -1)
{ perror("listen");close(sfd);exit(-1); }

accept函数

接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求。(也就是说,类似于移动营业厅,如果有客户打电话给10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,也就是说,后面的所有操作,此时已经于服务器没有关系,而是话务员跟客户的交流。对应过来,客户请求连接我们的服务器,我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用accept函数产生一个新的套接字,然后用这个新的套接字跟我们的客户进行收发数据。也就是说,服务器跟一个客户端连接成功,会有两个套接字。)

原型:int accept(int s,struct sockaddr * addr,int * addrlen);

参数:

  • s 为前面socket的返回值.即sfd
  • addr 为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
  • addrlen 表示结构体的长度,为整型指针

返回值:成功则返回新的socket处理代码new_fd,失败返回-1

常用实例:

1
2
3
4
5
6
7
8
struct sockaddr_in clientaddr;

memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{perror("accept");close(sfd);exit(-1);}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

recv函数

用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间

原型:int recv(int sockfd,void *buf,int len,unsigned int flags);

参数:

  • sockfd 为前面accept的返回值.即new_fd,也就是新的套接字。
  • buf 表示缓冲区
  • len 表示缓冲区的长度
  • flags 通常为0

返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1

常用实例:

1
2
3
4
5
char buf[512] = {0};

if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{perror("recv");close(new_fd);close(sfd);exit(-1);}
puts(buf);

send函数

用新的套接字发送数据给指定的远端主机

原型:int send(int s,const void * msg,int len,unsigned int flags);

参数:

  • s 为前面accept的返回值.即new_fd
  • msg 一般为常量字符串
  • len 表示长度
  • flags 通常为0

返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1

常用实例:

1
2
if(send(new_fd, "hello", 6, 0) == -1)
{perror("send");close(new_fd);close(sfd);exit(-1);}

close函数

当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源

原型:int close(int fd);

参数:

  • fd 为前面的sfd,new_fd

返回值:若文件顺利关闭则返回0,发生错误时返回-1

常用实例:

1
close(new_fd);

客户端

connect函数

用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去。

原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);

参数:

  • sockfd 为前面socket的返回值,即sfd
  • serv_addr 为结构体指针变量,存储着远程服务器的IP与端口号信息。
  • addrlen 表示结构体变量的长度

返回值:成功则返回0,失败返回-1

常用实例:

1
2
3
4
5
6
7
struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服务器的ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{perror("connect");close(sfd);exit(-1);}

还可以不写客户端程序,使用telnet远程登录来检测我们的服务器端程序。比如我们的服务器程序在监听8888端口,我们可以用telnet 192.168.0.101 8888来查看服务端的状况。

Example:将一些通用的代码全部封装起来,以后要用直接调用函数即可。如下:

通用网络封装代码头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

tcp_net_socket.h

#ifndef __TCP__NET__SOCKET__H
#define __TCP__NET__SOCKET__H


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>

extern int tcp_init(const char* ip,int port);
extern int tcp_accept(int sfd);
extern int tcp_connect(const char* ip,int port);
extern void signalhandler(void);

#endif

具体的通用函数封装如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
tcp_net_socket.c

#include "tcp_net_socket.h"

int tcp_init(const char* ip, int port) //用于初始化操作
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);//首先创建一个socket,向系统申请
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);//或INADDR_ANY

if(bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)
//将新的socket与制定的ip、port绑定
{
perror("bind");
close(sfd);
exit(-1);
}

if(listen(sfd, 10) == -1)//监听它,并设置其允许最大的连接数为10个
{
perror("listen");
close(sfd);
exit(-1);
}
return sfd;
}

int tcp_accept(int sfd) //用于服务端的接收
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);

//sfd接受客户端连接,并创建新的socket为new_fd,将请求连接的客户端的ip、port保存在结构体clientaddr中

if(new_fd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect...\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
return new_fd;
}

int tcp_connect(const char* ip, int port) //用于客户端的连接
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);//向系统注册申请新的socket
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);

if(connect(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)
//将sfd连接至制定的服务器网络地址serveraddr
{
perror("connect");
close(sfd);
exit(-1);
}
return sfd;
}

void signalhandler(void) //用于信号处理,让服务端在按下Ctrl+c或Ctrl+\的时候不会退出
{
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet,SIGINT);
sigaddset(&sigSet,SIGQUIT);
sigprocmask(SIG_BLOCK,&sigSet,NULL);
}

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
tcp_net_server.c

#include "tcp_net_socket.h"

int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./servertcp ip port\n");
exit(-1);
}

signalhandler();

int sfd = tcp_init(argv[1], atoi(argv[2])); //或int sfd = tcp_init("192.168.0.164", 8888);

while(1) //用while循环表示可以与多个客户端接收和发送,但仍是阻塞模式的
{
int cfd = tcp_accept(sfd);
char buf[512] = {0};
if(recv(cfd, buf, sizeof(buf), 0) == -1)//从cfd客户端接收数据存于buf中
{
perror("recv");
close(cfd);
close(sfd);
exit(-1);
}

puts(buf);
if(send(cfd, "hello world", 12, 0) == -1)//从buf中取向cfd客户端发送数据
{
perror("send");
close(cfd);
close(sfd);
exit(-1);
}
close(cfd);
}
close(sfd);
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tcp_net_client.c

#include "tcp_net_socket.h"

int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("usage:./clienttcp ip port\n");
exit(-1);
}
int sfd = tcp_connect(argv[1],atoi(argv[2]));
char buf[512] = {0};
send(sfd, "hello", 6, 0); //向sfd服务端发送数据
recv(sfd, buf, sizeof(buf), 0); //从sfd服务端接收数据
puts(buf);
close(sfd);
}

1
2
3
4
5
6
7
#gcc –o tcp_net_server tcp_net_server.c tcp_net_socket.c

#gcc –o tcp_net_client tcp_net_client.c tcp_net_socket.c

#./tcp_net_server 192.168.0.164 8888

#./tcp_net_client 192.168.0.164 8888

上面的虽然可以实现多个客户端访问,但是仍然是阻塞模式(即一个客户访问的时候会阻塞不让另外的客户访问)。解决办法有:

多进程(因为开销比较大,所以不常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <tcp_net_socket.h>

int main()
{
int sfd = tcp_init("192.168.0.101", 8888);
while(1)
{
int cfd = tcp_accept(sfd);
if(fork() == 0)
{
send(cfd, "hello", 6, 0);
sleep(10);
close(cfd);
}
else
{
close(cfd);
}
}
close(sfd);
return 0;
}

多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <tcp_net_socket.h>
#include <pthread.h>

void* pthfunc(void* arg)
{
int cfd = (int)arg;
send(cfd, "hello", 6, 0);
sleep(10);
close(cfd);
}

int main()
{
int sfd = tcp_init("192.168.0.101", 8888);
pthread_t pthid = 0;
while(1)
{
int cfd = tcp_accept(sfd);
pthread_create(&pthid, NULL, pthfunc, (void*)cfd);
}
close(sfd);
return 0;
}

// 备注 读写大容量的文件时,通过下面的方法效率很高

ssize_t readn(int fd, char *buf, int size)//读大量内容
{
char *pbuf = buf;
int total ,nread;
for(total = 0; total < size; )
{
nread=read(fd,pbuf,size-total);
if(nread==0)
return total;
if(nread == -1)
{
if(errno == EINTR)
continue;
else
return -1;
}
total += nread;
pbuf += nread;
}
return total;
}

ssize_t writen(int fd, char *buf, int size)//写大量内容
{
char *pbuf=buf;
int total ,nwrite;
for(total = 0; total < size; )
{
nwrite=write(fd,pbuf,size-total);
if( nwrite <= 0 )
{
if( nwrite == -1 && errno == EINTR )
continue;
else
return -1;
}
total += nwrite;
pbuf += nwrite;
}
return total;
}

调用fcntl将sockfd设置为非阻塞模式。(不常见)

1
2
3
4
5
6
7
8
#include <unistd.h>
#include <fcntl.h>

……
sockfd = socket(AF_INET,SOCK_STREAM,0);
iflags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd,F_SETFL,O_NONBLOCK | iflags);
……

多路选择select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <sys/select.h>
#include "tcp_net_socket.h"
#define MAXCLIENT 10

main()
{
int sfd = tcp_init("192.168.0.164", 8888);
int fd = 0;
char buf[512] = {0};
fd_set rdset;
while(1)
{
FD_ZERO(&rdset);
FD_SET(sfd,&rdset);
if(select(MAXCLIENT + 1, &rdset, NULL, NULL, NULL) < 0)
continue;
for(fd = 0; fd < MAXCLIENT; fd++)
{
if(FD_ISSET(fd,&rdset))
{
if(fd == sfd)
{
int cfd = tcp_accept(sfd);
FD_SET(cfd,&rdset);
//……
}
else
{
bzero(buf, sizeof(buf));
recv(fd, buf, sizeof(buf), 0);
puts(buf);
send(fd, "java", 5, 0);
//FD_CLR(fd, &rdset);
close(fd);
}
}
}
}
close(sfd);
}

使用UDP协议的流程图

UDP通信流程图如下:

服务端:socket—-bind—-recvfrom—-sendto—-close

客户端:socket—————sendto—-recvfrom—-close

sendto()函数原型:

1
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);

该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。

recvfrom()函数原型:

1
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);

from是一个struct sockaddr类型的变量,该变量保存连接机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。

Example:UDP的基本操作

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

main()
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}

char buf[512] = {0};
while(1)
{
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);

sendto(sfd, "world", 6, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
}

close(sfd);
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}

struct sockaddr_in toaddr;
bzero(&toaddr, sizeof(toaddr));
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(atoi(argv[2])); //此处的端口号要跟服务器一样
toaddr.sin_addr.s_addr = inet_addr(argv[1]); //此处为服务器的ip

sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));

char buf[512] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
close(sfd);
}

Example:UDP发送文件 先发文件大小 再发文件内容

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

main()
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}

char buf[512] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);

FILE* fp = fopen("1.txt","rb");
struct stat st; //用于获取文件内容的大小
stat("1.txt", &st);
int filelen = st.st_size;
sendto(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
while(!feof(fp)) //表示没有到文件尾
{
int len = fread(buf,1,sizeof(buf),fp);
sendto(sfd, buf, len, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
}

close(sfd);
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFSIZE 512
int main(int argc, char* argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}

struct sockaddr_in toaddr;
bzero(&toaddr, sizeof(toaddr));
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(atoi(argv[2]));
toaddr.sin_addr.s_addr = inet_addr(argv[1]);
sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));

char buf[BUFSIZE] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
int filelen = 0; //用于保存文件长度
FILE* fp = fopen("2.txt","w+b");

//接收文件的长度
recvfrom(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
printf("the length of file is %d\n",filelen);
printf("Create a new file!\n");
printf("begin to reveive file content!\n");
//接收文件的内容

while(1)
{
int len = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
if(len < BUFSIZE)
//如果接收的长度小于BUFSIZE,则表示最后一次接收,此时要用break退出循环
{
fwrite(buf,sizeof(char),len,fp);
break;
}
fwrite(buf,sizeof(char),len,fp);
}
printf("receive file finished!\n");
close(sfd);
}

设置套接口的选项setsockopt的用法

函数原型:

1
2
3
4
#include <sys/types.h >
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

  • sockfd:标识一个套接口的描述字
  • level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
  • optname:需设置的选项
  • optval:指针,指向存放选项值的缓冲区
  • optlen:optval缓冲区长度

全部都必须要放在bind之前,另外通常是用于UDP的。

如果在已经处于 ESTABLISHED状态下的socket(一般由端口号和标志符区分)调用closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:

1
2
int reuse=1;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));

如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历TIME_WAIT的过程:

1
2
int reuse=0;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));

在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:

1
2
3
4
5
int nNetTimeout=1000; // 1秒
// 发送时限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
// 接收时限
setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));

在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步),系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:

1
2
3
4
5
6
// 接收缓冲区
int nRecvBuf=32*1024; // 设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
// 发送缓冲区
int nSendBuf=32*1024; // 设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

如果在发送数据时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能:

1
2
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(int));

同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):

1
2
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));

一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:

1
2
int bBroadcast = 1;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(int));

单播、广播、组播(多播)

多播广播是用于建立分步式系统:例如网络游戏、ICQ聊天构建、远程视频会议系统的重要工具。使用多播广播的程序和UDP的单播程序相似。区别在于多播广播程序使用特殊的IP地址。

对于单播而言,单播用于两个主机之间的端对端通信。

对于广播而言,广播用于一个主机对整个局域网上所有主机上的数据通信。广播只能用于客户机向服务器广播,因为客户机要指明广播的IP地址“192.168.0.255”和广播的端口号。服务器端bing的时候,绑定的端口号要跟广播的端口号是同一个。这样才能收到广播消息。实例请参考《udp_广播》。

对于多播而言,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。单播和广播是两个极端,要么对一个主机进行通信,要么对整个局域网上的主机进行通信。实际情况下,经常需要对一组特定的主机进行通信,而不是整个局域网上的所有主机,这就是多播的用途。例如,我们通常所说的讨论组。IPv4多播地址采用D类IP地址确定多播的组。在Internet中,多播地址范围是从224.0.0.0到234.255.255.255。

多播的程序设计也要使用setsockopt()函数和getsockopt()函数来实现。其中对于setsockopt的第二个参数level不再是SOL_SOCKET,而是IPPROTO_IP;而且第三个参数optname常见的选项有:

  • IP_ADD_MEMBERSHIP:在指定接口上加入组播组
  • IP_DROP_MEMBERSHIP:退出组播组

选项IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP加入或者退出一个组播组,通过选项IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP,对一个结构struct ip_mreq类型的变量进行控制。

struct ip_mreq原型如下:

1
2
3
4
5
struct ip_mreq
{
struct in_addr imr_multiaddr; /*加入或者退出的多播组IP地址*/
struct in_addr imr_interface; /*加入或者退出的网络接口IP地址,本机IP*/
};

选项IP_ADD_MEMBERSHIP用于加入某个多播组,之后就可以向这个多播组发送数据或者从多播组接收数据。此选项的值为mreq结构,成员imr_multiaddr是需要加入的多播组IP地址,成员imr_interface是本机需要加入多播组的网络接口IP地址。例如:

1
2
3
4
5
6
7
8
9
struct ip_mreq mreq;
memset(&mreq, 0, sizeof(struct ip_mreq));
mreq.imr_interface.s_addr = INADDR_ANY;
mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1");
if(-1 == setsockopt(sfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)))
{
perror("setsockopt");
exit(-1);
}

接下来再绑定组播的port号(如65000),就可以接收组播消息了。

选项IP_ADD_MEMBERSHIP每次只能加入一个网络接口的IP地址到多播组,但并不是一个多播组仅允许一个主机IP地址加入,可以多次调用IP_ADD_MEMBERSHIP选项来实现多个IP地址加入同一个广播组,或者同一个IP地址加入多个广播组。

选项IP_DROP_MEMBERSHIP用于从一个多播组中退出。例如:

1
2
3
4
5
if(-1 == setsockopt(sfd, IPPROTP_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)))
{
perror("setsockopt");
exit(-1);
}

100个网络基础知识

OpenMPI结构

Open MPI 联合了四种MPI的不同实现:

  • LAM/MPI,
  • LA/MPI (Los Alamos MPI)
  • FT-MPI (Fault-Tolerant MPI)
  • PACX-MPI

Architecture

Open MPI使用C语言编写,是一个非常庞大、复杂的代码库。2003的MPI 标准——MPI-2.0,定义了超过300个API接口。

之前的4个项目,每个项目都非常庞大。例如,LAM/MPI由超过1900个源码文件,代码量超过30W行。希望Open MPI尽可能的支持更多的特性、环境以及网络类型。因此Open MPI花了大量时间设计架构,主要专注于三件事情:

  • 将相近的功能划分在不同的抽象层
  • 使用运行时可加载的插件以及运行时参数,来选择相同接口的不同实现
  • 不允许抽象影响性能

Abstraction Layer Architecture

Open MPi 可以分为三个主要的抽象层,自顶向下依次为:

  • OMPI (Open MPI) (pronounced: oom-pee):
    • 由 MPI standard 所定义
    • 暴露给上层应用的 API,由外部应用调用
  • ORTE (Open MPI Run-Time Environment) (pronounced “or-tay”):
    • MPI 的 run-time system
      • launch, monitor, kill individual processes
      • Group individual processes into “jobs”
    • 重定向stdin、stdout、stderr
    • ORTE 进程管理方式:在简单的环境中,通过rsh或ssh 来launch 进程。而复杂环境(HPC专用)会有shceduler、resource manager等管理组件,面向多个用户进行公平的调度以及资源分配,ORTE支持多种管理环境,例如,orque/PBS Pro, SLURM, Oracle Grid Engine, and LSF.
      • 注意 ORTE 在 5.x 版本中被移除,进程管理模块被替换成了prrte (github.com))
  • OPAL (Open, Portable Access Layer) (pronounced: o-pull): OPAL 是xOmpi的最底层
    • 只作用于单个进程
    • 负责不同环境的可移植性
    • 包含了一些通用功能(例如链表、字符串操作、debug控制等等)

在代码目录中是以project的形式存在,也就是

1
2
3
4
ompi/
├── ompi
├── opal
└── orte

需要注意的时,考虑到性能因素,Open MPI 有中“旁路”机制(bypass),ORTE以及OMPI层,可以绕过OPAL,直接与操作系统(甚至是硬件)进行交互。例如OMPI会直接与网卡进行交互,从而达到最大的网络性能。

Plugin Architecture

为了在 Open MPI 中使用类似但是不同效果的功能,Open MPI 设计一套被称为Modular Component Architecture (MCA)的架构。在MCA架构中,为每一个抽象层(也就是OMPI、ORTE、OPAL)定义了多个framework,这里的framework类似于其他语言语境中的接口(interface),framework对于一个功能进行了抽象,而plugin就是对于一个framework的不同实现。每个 Plugin 都是以动态链接库(DSO,dynamic shared object)的形式存在。因此run time 能够动态的加载不同的plugin。

例如下图中 btl 是一个功能传输bytes的framework,它属于OMPI层,btl framework之下又包含针对不同网络类型的实现,例如 tcp、openib (InfiniBand)、sm (shared memory)、sm-cuda (shared memory for CUDA)

PML

PML即P2P Management Layer,MPI基于这一层,基本所有的通信都是通过这一层实现的,它提供 MPI 层所需的 P2P 接口功能的 MCA 组件类型。 PML 是一个相对较薄的层,主要用于通过多种传输(字节传输层 (BTL) MCA 组件类型的实例)对消息进行分段和调度,如下所示:

1
2
3
4
5
6
7
------------------------------------
| MPI |
------------------------------------
| PML |
------------------------------------
| BTL (TCP) | BTL (SM) | BTL (...) |
------------------------------------

MCA 框架在库初始化期间选择单个 PML 组件。 最初,所有可用的 PML 都被加载(可能作为共享库)并调用它们的组件打开和初始化函数。 MCA 框架选择返回最高优先级的组件并关闭/卸载可能已打开的任何其他 PML 组件。

在初始化所有 MCA 组件之后,MPI/RTE 将对 PML 进行向下调用,以提供进程的初始列表(ompi_proc_t 实例)和更改通知(添加/删除)。PML 模块必须选择一组用于达到给定目的地的 BTL 组件。这些应缓存在挂在 ompi_proc_t 之外的 PML 特定数据结构上,也就是说PML层应该给它定义的一系列通信函数指针赋值,让PML层知道该调用哪些函数。然后,PML 应该应用调度算法(循环、加权分布等)来调度可用 BTL 上的消息传递。

MTL

Matching Transport Layer匹配传输层 (MTL) 为通过支持硬件/库消息匹配的设备传输 MPI 点对点消息提供设备层支持。该层与 MTL PML 组件一起使用,以在给定架构上提供最低延迟和最高带宽。 上层不提供其他 PML 接口中的功能,例如消息分段、多设备支持和 NIC 故障转移。 通常,此接口不应用于传输层支持。 相反,应该使用 BTL 接口。 BTL 接口允许在多个用户之间进行多路复用(点对点、单面等),并提供了该接口中没有的许多功能(来自任意缓冲区的 RDMA、主动消息传递、合理的固定内存缓存等)

这应该是一个接口层,负责调用底层真正通信的函数。

阻塞发送(调用不应该返回,直到用户缓冲区可以再次使用)。此调用必须满足标准 MPI 语义,如 mode 参数中所要求的。有一个特殊的模式参数,MCA_PML_BASE_SEND_COMPLETE,它需要在函数返回之前本地完成。这是对集体惯例的优化,否则会导致基于广播的集体的性能退化。

Open MPI 是围绕非阻塞操作构建的。此功能适用于在不定期触发进度功能的情况下可能发生点对点之外的进展事件(例如,集体、I/O、单面)的网络。

虽然 MPI 不允许用户指定否定标签,但它们在 Open MPI 内部用于为集体操作提供独特的渠道。因此,如果使用否定标签,MTL 不会导致错误。

非阻塞发送到对等方。此调用必须满足标准 MPI 语义,如 mode 参数中所要求的。有一个特殊的模式参数,MCA_PML_BASE_SEND_COMPLETE,它需要在请求被标记为完成之前本地完成。

PML 将处理请求的创建,将模块结构中请求的字节数直接放在 ompi_request_t 结构之后可用于 MTL。一旦可以安全地销毁请求(它已通过调用 REQUEST_FReE 或 TEST/WAIT 完成并释放),PML 将处理请求的适当销毁。当请求被标记为已完成时,MTL 应删除与请求关联的所有资源。

虽然 MPI 不允许用户指定否定标签,但它们在 Open MPI 内部用于为集体操作提供独特的渠道。因此,如果使用否定标签,MTL 不会导致错误。

OSC

One-sided Communication(OSC) 用于实现 MPI-2 标准的单向通信章节的接口。 在范围上类似于来自 MPI-1 的点对点通信的 PML。有以下几个主要函数:

  • OSC component initialization:初始化给定的单边组件。 此函数应初始化任何组件级数据。组件框架不会延迟打开,因此应尽量减少在此功能期间分配的内存量。
  • OSC component finalization:结束给定的单边组件。 此函数应清除在 component_init() 期间分配的任何组件级数据。 它还应该清理在组件生命周期内创建的任何数据,包括任何未完成的模块。
  • OSC component query:查询给定info和comm,组件是否可以用于单边通信。 能够将组件用于窗口并不意味着该组件将被选中。 在此调用期间不应修改 win 参数,并且不应分配与此窗口关联的内存。
  • OSC component select:已选择此组件来为给定窗口提供单方面的服务。 win->w_osc_module 字段可以更新,内存可以与此窗口相关联。 该模块应在此函数返回后立即准备好使用,并且该模块负责在调用结束之前提供任何所需的集体同步。comm 是用户指定的通信器,因此适用正常的内部使用规则。 换句话说,如果您需要在窗口的生命周期内进行通信,则应在此函数期间调用 comm_dup()。

MPI_Init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

static const char FUNC_NAME[] = "MPI_Init";

int MPI_Init(int *argc, char ***argv)
{
int err;
int provided;
char *env;
int required = MPI_THREAD_SINGLE;

/* check for environment overrides for required thread level. If
there is, check to see that it is a valid/supported thread level.
If not, default to MPI_THREAD_MULTIPLE. */

if (NULL != (env = getenv("OMPI_MPI_THREAD_LEVEL"))) {
required = atoi(env);
if (required < MPI_THREAD_SINGLE || required > MPI_THREAD_MULTIPLE) {
required = MPI_THREAD_MULTIPLE;
}
}
// 检查多线程相关的命令行参数

/* Call the back-end initialization function (we need to put as
little in this function as possible so that if it's profiled, we
don't lose anything) 这个函数在下边了
*/

if (NULL != argc && NULL != argv) {
err = ompi_mpi_init(*argc, *argv, required, &provided, false);
} else {
err = ompi_mpi_init(0, NULL, required, &provided, false);
}

/* Since we don't have a communicator to invoke an errorhandler on
here, don't use the fancy-schmancy ERRHANDLER macros; they're
really designed for real communicator objects. Just use the
back-end function directly. */

if (MPI_SUCCESS != err) {
return ompi_errhandler_invoke(NULL, NULL,
OMPI_ERRHANDLER_TYPE_COMM,
err <
0 ? ompi_errcode_get_mpi_code(err) :
err, FUNC_NAME);
} // 如果初始化函数返回的不是 MPI_SUCCESS, 就返回错误码

SPC_INIT(); // 初始化调用函数的计时器

return MPI_SUCCESS;
}

ompi_mpi_init是真正mpi初始化的函数。内部设计的很精细,因为要考虑很多多线程同时操作的情况,在各个地方都加了锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
int ompi_mpi_init(int argc, char **argv, int requested, int *provided,
bool reinit_ok)
{
int ret;
char *error = NULL;
#if OPAL_USING_INTERNAL_PMIX
char *evar;
#endif
volatile bool active;
bool background_fence = false;
pmix_info_t info[2];
pmix_status_t rc;
OMPI_TIMING_INIT(64);

ompi_hook_base_mpi_init_top(argc, argv, requested, provided);

/* Ensure that we were not already initialized or finalized. */
int32_t expected = OMPI_MPI_STATE_NOT_INITIALIZED;
int32_t desired = OMPI_MPI_STATE_INIT_STARTED;
opal_atomic_wmb(); // 内存同步?
if (!opal_atomic_compare_exchange_strong_32(&ompi_mpi_state, &expected,
desired)) {
// 此内置函数实现了原子比较和交换操作。这会将 ompi_mpi_state 的内容与 expected 的内容进行比较。
// 如果相等,则该操作是将 desired 写入 ompi_mpi_state。
// 如果它们不相等,操作是读取和 ompi_mpi_state 写入 expected。

// 避免多个进程/线程同时修改当前MPI状态
// If we failed to atomically transition ompi_mpi_state from
// NOT_INITIALIZED to INIT_STARTED, then someone else already
// did that, and we should return.
if (expected >= OMPI_MPI_STATE_FINALIZE_STARTED) {
opal_show_help("help-mpi-runtime.txt",
"mpi_init: already finalized", true);
return MPI_ERR_OTHER;
} else if (expected >= OMPI_MPI_STATE_INIT_STARTED) {
// In some cases (e.g., oshmem_shmem_init()), we may call
// ompi_mpi_init() multiple times. In such cases, just
// silently return successfully once the initializing
// thread has completed.
if (reinit_ok) {
while (ompi_mpi_state < OMPI_MPI_STATE_INIT_COMPLETED) {
usleep(1);
}
return MPI_SUCCESS;
}

opal_show_help("help-mpi-runtime.txt",
"mpi_init: invoked multiple times", true);
return MPI_ERR_OTHER;
}
}

/* deal with OPAL_PREFIX to ensure that an internal PMIx installation
* is also relocated if necessary */
#if OPAL_USING_INTERNAL_PMIX
if (NULL != (evar = getenv("OPAL_PREFIX"))) {
opal_setenv("PMIX_PREFIX", evar, true, &environ);
}
#endif

ompi_mpi_thread_level(requested, provided); // 设置线程级别

ret = ompi_mpi_instance_init (*provided, &ompi_mpi_info_null.info.super, MPI_ERRORS_ARE_FATAL, &ompi_mpi_instance_default);
// 创建一个新的MPI实例,

if (OPAL_UNLIKELY(OMPI_SUCCESS != ret)) {
error = "ompi_mpi_init: ompi_mpi_instance_init failed";
goto error;
}

ompi_hook_base_mpi_init_top_post_opal(argc, argv, requested, provided);

/* initialize communicator subsystem,
communicator MPI_COMM_WORLD and MPI_COMM_SELF

构建通信域结构体,保存进程数信息
通过ompi_group_translate_ranks函数得到rank
通过遍历找到通信域内与本进程对应的rank么
*/
if (OMPI_SUCCESS != (ret = ompi_comm_init_mpi3 ())) {
error = "ompi_mpi_init: ompi_comm_init_mpi3 failed";
goto error;
}

/* Bozo argument check */
if (NULL == argv && argc > 1) {
ret = OMPI_ERR_BAD_PARAM;
error = "argc > 1, but argv == NULL";
goto error;
}

/* if we were not externally started, then we need to setup
* some envars so the MPI_INFO_ENV can get the cmd name
* and argv (but only if the user supplied a non-NULL argv!), and
* the requested thread level
*/
if (NULL == getenv("OMPI_COMMAND") && NULL != argv && NULL != argv[0]) {
opal_setenv("OMPI_COMMAND", argv[0], true, &environ);
}
if (NULL == getenv("OMPI_ARGV") && 1 < argc) {
char *tmp;
tmp = opal_argv_join(&argv[1], ' ');
opal_setenv("OMPI_ARGV", tmp, true, &environ);
free(tmp);
}

#if (OPAL_ENABLE_TIMING)
if (OMPI_TIMING_ENABLED && !opal_pmix_base_async_modex &&
opal_pmix_collect_all_data && !ompi_singleton) {
if (PMIX_SUCCESS != (rc = PMIx_Fence(NULL, 0, NULL, 0))) {
ret = opal_pmix_convert_status(rc);
error = "timing: pmix-barrier-1 failed";
goto error;
}
OMPI_TIMING_NEXT("pmix-barrier-1");
if (PMIX_SUCCESS != (rc = PMIx_Fence(NULL, 0, NULL, 0))) {
ret = opal_pmix_convert_status(rc);
error = "timing: pmix-barrier-2 failed";
goto error;
}
OMPI_TIMING_NEXT("pmix-barrier-2");
}
#endif

if (!ompi_singleton) {
if (opal_pmix_base_async_modex) {
/* if we are doing an async modex, but we are collecting all
* data, then execute the non-blocking modex in the background.
* All calls to modex_recv will be cached until the background
* modex completes. If collect_all_data is false, then we skip
* the fence completely and retrieve data on-demand from the
* source node.
*/
if (opal_pmix_collect_all_data) {
/* execute the fence_nb in the background to collect
* the data */
background_fence = true;
active = true;
OPAL_POST_OBJECT(&active);
PMIX_INFO_LOAD(&info[0], PMIX_COLLECT_DATA, &opal_pmix_collect_all_data, PMIX_BOOL);
if( PMIX_SUCCESS != (rc = PMIx_Fence_nb(NULL, 0, NULL, 0,
fence_release,
(void*)&active))) {
ret = opal_pmix_convert_status(rc);
error = "PMIx_Fence_nb() failed";
goto error;
}
}
} else {
/* we want to do the modex - we block at this point, but we must
* do so in a manner that allows us to call opal_progress so our
* event library can be cycled as we have tied PMIx to that
* event base */
active = true;
OPAL_POST_OBJECT(&active);
PMIX_INFO_LOAD(&info[0], PMIX_COLLECT_DATA, &opal_pmix_collect_all_data, PMIX_BOOL);
rc = PMIx_Fence_nb(NULL, 0, info, 1, fence_release, (void*)&active);
if( PMIX_SUCCESS != rc) {
ret = opal_pmix_convert_status(rc);
error = "PMIx_Fence() failed";
goto error;
}
/* cannot just wait on thread as we need to call opal_progress */
OMPI_LAZY_WAIT_FOR_COMPLETION(active);
}
}

OMPI_TIMING_NEXT("modex");

// 把当前这两个通信域加进来
MCA_PML_CALL(add_comm(&ompi_mpi_comm_world.comm));
MCA_PML_CALL(add_comm(&ompi_mpi_comm_self.comm));

// 这是fault tolerant相关的结构
#if OPAL_ENABLE_FT_MPI
/* initialize the fault tolerant infrastructure (revoke, detector,
* propagator) */
if( ompi_ftmpi_enabled ) {
const char *evmethod;
rc = ompi_comm_rbcast_init();
if( OMPI_SUCCESS != rc ) return rc;
rc = ompi_comm_revoke_init();
if( OMPI_SUCCESS != rc ) return rc;
rc = ompi_comm_failure_propagator_init();
if( OMPI_SUCCESS != rc ) return rc;
rc = ompi_comm_failure_detector_init();
if( OMPI_SUCCESS != rc ) return rc;

evmethod = event_base_get_method(opal_sync_event_base);
if( 0 == strcmp("select", evmethod) ) {
opal_show_help("help-mpi-ft.txt", "module:event:selectbug", true);
}
}
#endif

/*
* Dump all MCA parameters if requested
*/
if (ompi_mpi_show_mca_params) {
ompi_show_all_mca_params(ompi_mpi_comm_world.comm.c_my_rank,
ompi_process_info.num_procs,
ompi_process_info.nodename);
}

/* Do we need to wait for a debugger? */
ompi_rte_wait_for_debugger();

/* Next timing measurement */
OMPI_TIMING_NEXT("modex-barrier");

if (!ompi_singleton) {
/* if we executed the above fence in the background, then
* we have to wait here for it to complete. However, there
* is no reason to do two barriers! */
if (background_fence) {
OMPI_LAZY_WAIT_FOR_COMPLETION(active);
} else if (!ompi_async_mpi_init) {
/* wait for everyone to reach this point - this is a hard
* barrier requirement at this time, though we hope to relax
* it at a later point */
bool flag = false;
active = true;
OPAL_POST_OBJECT(&active);
PMIX_INFO_LOAD(&info[0], PMIX_COLLECT_DATA, &flag, PMIX_BOOL);
if (PMIX_SUCCESS != (rc = PMIx_Fence_nb(NULL, 0, info, 1,
fence_release, (void*)&active))) {
ret = opal_pmix_convert_status(rc);
error = "PMIx_Fence_nb() failed";
goto error;
}
OMPI_LAZY_WAIT_FOR_COMPLETION(active);
}
}

/* check for timing request - get stop time and report elapsed
time if so, then start the clock again */
OMPI_TIMING_NEXT("barrier");

#if OPAL_ENABLE_PROGRESS_THREADS == 0
/* Start setting up the event engine for MPI operations. Don't
block in the event library, so that communications don't take
forever between procs in the dynamic code. This will increase
CPU utilization for the remainder of MPI_INIT when we are
blocking on RTE-level events, but may greatly reduce non-TCP
latency. */
int old_event_flags = opal_progress_set_event_flag(0);
opal_progress_set_event_flag(old_event_flags | OPAL_EVLOOP_NONBLOCK);
#endif

/* wire up the mpi interface, if requested. Do this after the
non-block switch for non-TCP performance. Do before the
polling change as anyone with a complex wire-up is going to be
using the oob.
预先执行一些MPI send recv,建立连接?
*/
if (OMPI_SUCCESS != (ret = ompi_init_preconnect_mpi())) {
error = "ompi_mpi_do_preconnect_all() failed";
goto error;
}

/* Init coll for the comms. This has to be after dpm_base_select,
(since dpm.mark_dyncomm is not set in the communicator creation
function else), but before dpm.dyncom_init, since this function
might require collective for the CID allocation.
设置集合通信相关的函数指针
*/
if (OMPI_SUCCESS !=
(ret = mca_coll_base_comm_select(MPI_COMM_WORLD))) {
error = "mca_coll_base_comm_select(MPI_COMM_WORLD) failed";
goto error;
}

if (OMPI_SUCCESS !=
(ret = mca_coll_base_comm_select(MPI_COMM_SELF))) {
error = "mca_coll_base_comm_select(MPI_COMM_SELF) failed";
goto error;
}

#if OPAL_ENABLE_FT_MPI
/* start the failure detector */
if( ompi_ftmpi_enabled ) {
rc = ompi_comm_failure_detector_start();
if( OMPI_SUCCESS != rc ) return rc;
}
#endif

/* Check whether we have been spawned or not. We introduce that
at the very end, since we need collectives, datatypes, ptls
etc. up and running here....
此例程检查应用程序是否已由另一个 MPI 应用程序生成,或者是否已独立启动。
如果它已经产生,它建立父通信器。
由于例程必须进行通信,因此它应该是 MPI_Init 的最后一步,以确保一切都已设置好。
*/
if (OMPI_SUCCESS != (ret = ompi_dpm_dyn_init())) {
return ret;
}

/* Fall through */
error:
if (ret != OMPI_SUCCESS) {
/* Only print a message if one was not already printed */
if (NULL != error && OMPI_ERR_SILENT != ret) {
const char *err_msg = opal_strerror(ret);
opal_show_help("help-mpi-runtime.txt",
"mpi_init:startup:internal-failure", true,
"MPI_INIT", "MPI_INIT", error, err_msg, ret);
}
ompi_hook_base_mpi_init_error(argc, argv, requested, provided);
OMPI_TIMING_FINALIZE;
return ret;
}

/* All done. Wasn't that simple? */
opal_atomic_wmb();
opal_atomic_swap_32(&ompi_mpi_state, OMPI_MPI_STATE_INIT_COMPLETED);
// 原子性地设置标志位为已完成初始化

/* Finish last measurement, output results
* and clear timing structure */
OMPI_TIMING_NEXT("barrier-finish");
OMPI_TIMING_OUT;
OMPI_TIMING_FINALIZE;

ompi_hook_base_mpi_init_bottom(argc, argv, requested, provided);

return MPI_SUCCESS;
}

这里分别搞了两个communicator,分别是word和self,communicator有以下的状态,看英文就能看出来意思,通过位运算设置状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define OMPI_COMM_INTER        0x00000001
#define OMPI_COMM_NAMEISSET 0x00000002
#define OMPI_COMM_INTRINSIC 0x00000004
#define OMPI_COMM_DYNAMIC 0x00000008
#define OMPI_COMM_ISFREED 0x00000010
#define OMPI_COMM_INVALID 0x00000020
#define OMPI_COMM_CART 0x00000100
#define OMPI_COMM_GRAPH 0x00000200
#define OMPI_COMM_DIST_GRAPH 0x00000400
#define OMPI_COMM_PML_ADDED 0x00001000
#define OMPI_COMM_EXTRA_RETAIN 0x00004000
#define OMPI_COMM_MAPBY_NODE 0x00008000
#define OMPI_COMM_GLOBAL_INDEX 0x00010000

MPI_Comm_rank

MPI_Comm_rank是获得进程在通信域的rank。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int MPI_Comm_rank(MPI_Comm comm, int *rank)
{
MEMCHECKER(
memchecker_comm(comm);
);

if ( MPI_PARAM_CHECK ) {
OMPI_ERR_INIT_FINALIZE(FUNC_NAME);
// 需要检查MPI是否已经初始化完成了,MPI通信域是不是合法的通信域,rank指针是否是空指针。

// MPI-2:4.12.4 明确指出 MPI_*_C2F 和 MPI_*_F2C 函数应将 MPI_COMM_NULL 视为有效的通信器
// openmpi将 ompi_comm_invalid() 保留为原始编码——根据 MPI-1 定义,其中 MPI_COMM_NULL 是无效的通信域。
// 因此,MPI_Comm_c2f() 函数调用 ompi_comm_invalid() 但也显式检查句柄是否为 MPI_COMM_NULL。
if (ompi_comm_invalid (comm))
return OMPI_ERRHANDLER_NOHANDLE_INVOKE(MPI_ERR_COMM,
FUNC_NAME);

if ( NULL == rank )
return OMPI_ERRHANDLER_INVOKE(comm, MPI_ERR_ARG,
FUNC_NAME);
}

*rank = ompi_comm_rank((ompi_communicator_t*)comm);
return MPI_SUCCESS;

ompi_comm_rank这个函数主要是返回结构体ompi_communicator_t的变量,结构体ompi_communicator_t如下,包括了集合通信,笛卡尔结构相关的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

struct ompi_communicator_t {
opal_infosubscriber_t super;
opal_mutex_t c_lock; /* 互斥锁,为了修改变量用的可能 */
char c_name[MPI_MAX_OBJECT_NAME]; /* 比如MPI_COMM_WORLD之类的 */
ompi_comm_extended_cid_t c_contextid;
ompi_comm_extended_cid_block_t c_contextidb;
uint32_t c_index;
int c_my_rank;
uint32_t c_flags; /* flags, e.g. intercomm,
topology, etc. */
uint32_t c_assertions; /* info assertions */
int c_id_available; /* the currently available Cid for allocation
to a child*/
int c_id_start_index; /* the starting index of the block of cids
allocated to this communicator*/
uint32_t c_epoch; /* Identifier used to differenciate between two communicators
using the same c_contextid (not at the same time, obviously) */

ompi_group_t *c_local_group;
ompi_group_t *c_remote_group; // 应该是存储了属于这个通信组的proc?

struct ompi_communicator_t *c_local_comm; /* a duplicate of the
local communicator in
case the comm is an
inter-comm*/

/* Attributes */
struct opal_hash_table_t *c_keyhash;


// 这些应该是笛卡尔结构相关的
/**< inscribing cube dimension */
int c_cube_dim;

/* Standard information about the selected topology module (or NULL
if this is not a cart, graph or dist graph communicator) */
struct mca_topo_base_module_t* c_topo;

/* index in Fortran <-> C translation array */
int c_f_to_c_index;

#ifdef OMPI_WANT_PERUSE
/*
* Place holder for the PERUSE events.
*/
struct ompi_peruse_handle_t** c_peruse_handles;
#endif

/* Error handling. This field does not have the "c_" prefix so
that the OMPI_ERRHDL_* macros can find it, regardless of whether
it's a comm, window, or file. */

ompi_errhandler_t *error_handler;
ompi_errhandler_type_t errhandler_type;

/* Hooks for PML to hang things */
struct mca_pml_comm_t *c_pml_comm;

/* Hooks for MTL to hang things */
struct mca_mtl_comm_t *c_mtl_comm;

/* Collectives module interface and data */
mca_coll_base_comm_coll_t *c_coll;

/* Non-blocking collective tag. These tags might be shared between
* all non-blocking collective modules (to avoid message collision
* between them in the case where multiple outstanding non-blocking
* collective coexists using multiple backends).
* 非阻塞的集合通信
*/
opal_atomic_int32_t c_nbc_tag;

/* instance that this comm belongs to */
ompi_instance_t* instance;

#if OPAL_ENABLE_FT_MPI
/** MPI_ANY_SOURCE Failed Group Offset - OMPI_Comm_failure_get_acked */
int any_source_offset;
/** agreement caching info for topology and previous returned decisions */
opal_object_t *agreement_specific;
/** Are MPI_ANY_SOURCE operations enabled? - OMPI_Comm_failure_ack */
bool any_source_enabled;
/** Has this communicator been revoked - OMPI_Comm_revoke() */
bool comm_revoked;
/** Force errors to collective pt2pt operations? */
bool coll_revoked;
#endif /* OPAL_ENABLE_FT_MPI */
};
typedef struct ompi_communicator_t ompi_communicator_t;

保存属于这个通信组的进程,有四种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Group structure
* Currently we have four formats for storing the process pointers that are members
* of the group.
* PList: a dense format that stores all the process pointers of the group.
* Sporadic: a sparse format that stores the ranges of the ranks from the parent group,
* that are included in the current group.
* Strided: a sparse format that stores three integers that describe a red-black pattern
* that the current group is formed from its parent group.
* Bitmap: a sparse format that maintains a bitmap of the included processes from the
* parent group. For each process that is included from the parent group
* its corresponding rank is set in the bitmap array.
*/
struct ompi_group_t {
opal_object_t super; /**< base class */
int grp_proc_count; /**< number of processes in group */
int grp_my_rank; /**< rank in group */
int grp_f_to_c_index; /**< index in Fortran <-> C translation array */
struct ompi_proc_t **grp_proc_pointers;
/**< list of pointers to ompi_proc_t structures
for each process in the group */
uint32_t grp_flags; /**< flags, e.g. freed, cannot be freed etc.*/
/** pointer to the original group when using sparse storage */
struct ompi_group_t *grp_parent_group_ptr;
union
{
struct ompi_group_sporadic_data_t grp_sporadic;
struct ompi_group_strided_data_t grp_strided;
struct ompi_group_bitmap_data_t grp_bitmap;
} sparse_data;

ompi_instance_t *grp_instance; /**< instance this group was allocated within */
};

MPI_Abort

MPI_Abort主要是打印错误信息后等待退出所有进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

int
ompi_mpi_abort(struct ompi_communicator_t* comm,
int errcode)
{
const char *host;
pid_t pid = 0;

/* Protection for recursive invocation */
if (have_been_invoked) {
return OMPI_SUCCESS;
}
have_been_invoked = true;

/* If MPI is initialized, we know we have a runtime nodename, so
use that. Otherwise, call opal_gethostname. */
if (ompi_rte_initialized) {
host = ompi_process_info.nodename;
} else {
host = opal_gethostname();
}
pid = getpid();

/* Should we print a stack trace? Not aggregated because they
might be different on all processes. */
if (opal_abort_print_stack) {
char **messages;
int len, i;

if (OPAL_SUCCESS == opal_backtrace_buffer(&messages, &len)) {
// 调用了linux内部的backtrace函数打印调用栈,需要#include <execinfo.h>
for (i = 0; i < len; ++i) {
fprintf(stderr, "[%s:%05d] [%d] func:%s\n", host, (int) pid,
i, messages[i]);
fflush(stderr);
}
free(messages);
} else {
/* This will print an message if it's unable to print the
backtrace, so we don't need an additional "else" clause
if opal_backtrace_print() is not supported. */
opal_backtrace_print(stderr, NULL, 1);
}
}

/* Wait for a while before aborting */
opal_delay_abort();

/* If the RTE isn't setup yet/any more, then don't even try
killing everyone. Sorry, Charlie... */
int32_t state = ompi_mpi_state;
if (!ompi_rte_initialized) {
fprintf(stderr, "[%s:%05d] Local abort %s completed successfully, but am not able to aggregate error messages, and not able to guarantee that all other processes were killed!\n",
host, (int) pid,
state >= OMPI_MPI_STATE_FINALIZE_STARTED ?
"after MPI_FINALIZE started" : "before MPI_INIT completed");
_exit(errcode == 0 ? 1 : errcode);
}

/* If OMPI is initialized and we have a non-NULL communicator,
then try to kill just that set of processes */
if (state >= OMPI_MPI_STATE_INIT_COMPLETED &&
state < OMPI_MPI_STATE_FINALIZE_PAST_COMM_SELF_DESTRUCT &&
NULL != comm) {
try_kill_peers(comm, errcode); /* kill only the specified groups, no return if it worked. */
}

/* We can fall through to here in a few cases:

1. The attempt to kill just a subset of peers via
try_kill_peers() failed.
2. MPI wasn't initialized, was already finalized, or we got a
NULL communicator.

In all of these cases, the only sensible thing left to do is to
kill the entire job. Wah wah. */
ompi_rte_abort(errcode, NULL);

/* Does not return - but we add a return to keep compiler warnings at bay*/
return 0;
}

MPI_Barrier

MPI_Barrier主要是检查参数之后调用coll_barrier。在两个进程的特例中,只有一个send-recv。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int MPI_Barrier(MPI_Comm comm)
{
int err = MPI_SUCCESS;

SPC_RECORD(OMPI_SPC_BARRIER, 1);

MEMCHECKER(
memchecker_comm(comm);
);

/* Error checking */

if (MPI_PARAM_CHECK) {
OMPI_ERR_INIT_FINALIZE(FUNC_NAME);
if (ompi_comm_invalid(comm)) {
return OMPI_ERRHANDLER_NOHANDLE_INVOKE(MPI_ERR_COMM, FUNC_NAME);
}
}

/* Intracommunicators: Only invoke the back-end coll module barrier
function if there's more than one process in the communicator */

if (OMPI_COMM_IS_INTRA(comm)) {
if (ompi_comm_size(comm) > 1) {
err = comm->c_coll->coll_barrier(comm, comm->c_coll->coll_barrier_module);
}
}

/* Intercommunicators -- always invoke, because, by definition,
there's always at least 2 processes in an intercommunicator. */

else {
err = comm->c_coll->coll_barrier(comm, comm->c_coll->coll_barrier_module);
}

/* All done */

OMPI_ERRHANDLER_RETURN(err, comm, err, FUNC_NAME);
}

coll_barrier应该是函数指针:

1
2
typedef int (*mca_coll_base_module_barrier_fn_t)
(struct ompi_communicator_t *comm, struct mca_coll_base_module_2_4_0_t *module);

函数指针可能的值有:

1
2
3
4
5
6
7
mca_coll_basic_barrier_inter_lin
ompi_coll_base_barrier_intra_basic_linear
mca_coll_basic_barrier_intra_log

mca_scoll_basic_barrier
mca_scoll_mpi_barrier
scoll_null_barrier

前三个是O(log(N))的,以mca_coll_basic_barrier_intra_log为例。这应该是将进程组织成树的形式,以位运算隐掉某一位来计算孩子进程号,通过send/recv空消息实现barrier。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
int
mca_coll_basic_barrier_intra_log(struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int i;
int err;
int peer;
int dim;
int hibit;
int mask;
int size = ompi_comm_size(comm);
int rank = ompi_comm_rank(comm);

/* Send null-messages up and down the tree. Synchronization at the
* root (rank 0). */

dim = comm->c_cube_dim;
hibit = opal_hibit(rank, dim);
--dim;

/* Receive from children. */

for (i = dim, mask = 1 << i; i > hibit; --i, mask >>= 1) {
peer = rank | mask;
if (peer < size) {
err = MCA_PML_CALL(recv(NULL, 0, MPI_BYTE, peer,
MCA_COLL_BASE_TAG_BARRIER,
comm, MPI_STATUS_IGNORE));
if (MPI_SUCCESS != err) {
return err;
}
}
// children就是比我大的或者等于我的
}

/* Send to and receive from parent. */

if (rank > 0) {
peer = rank & ~(1 << hibit);
err =
MCA_PML_CALL(send
(NULL, 0, MPI_BYTE, peer,
MCA_COLL_BASE_TAG_BARRIER,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) {
return err;
}

err = MCA_PML_CALL(recv(NULL, 0, MPI_BYTE, peer,
MCA_COLL_BASE_TAG_BARRIER,
comm, MPI_STATUS_IGNORE));
if (MPI_SUCCESS != err) {
return err;
}
// parent就是比自己小的,所以要把某一位变成0
}

/* Send to children. */

for (i = hibit + 1, mask = 1 << i; i <= dim; ++i, mask <<= 1) {
peer = rank | mask;
if (peer < size) {
err = MCA_PML_CALL(send(NULL, 0, MPI_BYTE, peer,
MCA_COLL_BASE_TAG_BARRIER,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) {
return err;
}
}
}

/* All done */

return MPI_SUCCESS;
}

这个直接是调用的allreduce,可省事了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* barrier_inter_lin
*
* Function: - barrier using O(log(N)) algorithm
* Accepts: - same as MPI_Barrier()
* Returns: - MPI_SUCCESS or error code
*/
int
mca_coll_basic_barrier_inter_lin(struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int rank;
int result;

rank = ompi_comm_rank(comm);
return comm->c_coll->coll_allreduce(&rank, &result, 1, MPI_INT, MPI_MAX,
comm, comm->c_coll->coll_allreduce_module);
}

ompi_coll_base_barrier_intra_basic_linear函数是从 BASIC coll 模块复制的,它不分割消息并且是简单的实现,但是对于一些少量节点和/或小数据大小,它们与基于树的分割操作一样快,因此可以选择这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int ompi_coll_base_barrier_intra_basic_linear(struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int i, err, rank, size, line;
ompi_request_t** requests = NULL;

size = ompi_comm_size(comm);
if( 1 == size )
return MPI_SUCCESS;
rank = ompi_comm_rank(comm);

/* All non-root send & receive zero-length message to root. */
if (rank > 0) {
err = MCA_PML_CALL(send (NULL, 0, MPI_BYTE, 0,
MCA_COLL_BASE_TAG_BARRIER,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

err = MCA_PML_CALL(recv (NULL, 0, MPI_BYTE, 0,
MCA_COLL_BASE_TAG_BARRIER,
comm, MPI_STATUS_IGNORE));
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}

/* The root collects and broadcasts the messages from all other process. */
else {
requests = ompi_coll_base_comm_get_reqs(module->base_data, size);
if( NULL == requests ) { err = OMPI_ERR_OUT_OF_RESOURCE; line = __LINE__; goto err_hndl; }

for (i = 1; i < size; ++i) {
err = MCA_PML_CALL(irecv(NULL, 0, MPI_BYTE, MPI_ANY_SOURCE,
MCA_COLL_BASE_TAG_BARRIER, comm,
&(requests[i])));
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}
err = ompi_request_wait_all( size-1, requests+1, MPI_STATUSES_IGNORE );
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
requests = NULL; /* we're done the requests array is clean */

for (i = 1; i < size; ++i) {
err = MCA_PML_CALL(send(NULL, 0, MPI_BYTE, i,
MCA_COLL_BASE_TAG_BARRIER,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}
}

/* All done */
return MPI_SUCCESS;
}

double ring方法在很多MPI算法里都有,barrier里也有double ring的实现。向左右的进程发送和接收数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int ompi_coll_base_barrier_intra_doublering(struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int rank, size, err = 0, line = 0, left, right;

size = ompi_comm_size(comm);
if( 1 == size )
return OMPI_SUCCESS;
rank = ompi_comm_rank(comm);

OPAL_OUTPUT((ompi_coll_base_framework.framework_output,"ompi_coll_base_barrier_intra_doublering rank %d", rank));

left = ((size+rank-1)%size);
right = ((rank+1)%size);

if (rank > 0) /* receive message from the left */
err = MCA_PML_CALL(recv((void*)NULL, 0, MPI_BYTE, left, MCA_COLL_BASE_TAG_BARRIER, comm, MPI_STATUS_IGNORE));

/* Send message to the right */
err = MCA_PML_CALL(send((void*)NULL, 0, MPI_BYTE, right, MCA_COLL_BASE_TAG_BARRIER, MCA_PML_BASE_SEND_STANDARD, comm));

/* root needs to receive from the last node */
if (rank == 0)
err = MCA_PML_CALL(recv((void*)NULL, 0, MPI_BYTE, left, MCA_COLL_BASE_TAG_BARRIER, comm, MPI_STATUS_IGNORE));

/* Allow nodes to exit */
if (rank > 0) /* post Receive from left */
err = MCA_PML_CALL(recv((void*)NULL, 0, MPI_BYTE, left, MCA_COLL_BASE_TAG_BARRIER, comm, MPI_STATUS_IGNORE));


/* send message to the right one */
err = MCA_PML_CALL(send((void*)NULL, 0, MPI_BYTE, right, MCA_COLL_BASE_TAG_BARRIER, MCA_PML_BASE_SEND_SYNCHRONOUS, comm));

/* rank 0 post receive from the last node */
if (rank == 0)
err = MCA_PML_CALL(recv((void*)NULL, 0, MPI_BYTE, left, MCA_COLL_BASE_TAG_BARRIER, comm, MPI_STATUS_IGNORE));

return MPI_SUCCESS;
}

还有一种先是把进程数调整到2的n次方,对于多余的进程先进行一次同步,再在进程之间两两交换通信,同样是根据位运算来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
* To make synchronous, uses sync sends and sync sendrecvs
*/

int ompi_coll_base_barrier_intra_recursivedoubling(struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int rank, size, adjsize, err, line, mask, remote;

size = ompi_comm_size(comm);
if( 1 == size )
return OMPI_SUCCESS;
rank = ompi_comm_rank(comm);
OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"ompi_coll_base_barrier_intra_recursivedoubling rank %d",
rank));

/* do nearest power of 2 less than size calc */
adjsize = opal_next_poweroftwo(size);
adjsize >>= 1;

/* if size is not exact power of two, perform an extra step */
if (adjsize != size) {
if (rank >= adjsize) {
/* send message to lower ranked node */
remote = rank - adjsize;
err = ompi_coll_base_sendrecv_zero(remote, MCA_COLL_BASE_TAG_BARRIER,
remote, MCA_COLL_BASE_TAG_BARRIER,
comm);
if (err != MPI_SUCCESS) { line = __LINE__; goto err_hndl;}

} else if (rank < (size - adjsize)) {

/* receive message from high level rank */
err = MCA_PML_CALL(recv((void*)NULL, 0, MPI_BYTE, rank+adjsize,
MCA_COLL_BASE_TAG_BARRIER, comm,
MPI_STATUS_IGNORE));

if (err != MPI_SUCCESS) { line = __LINE__; goto err_hndl;}
}
}

/* exchange messages */
if ( rank < adjsize ) {
mask = 0x1;
while ( mask < adjsize ) {
remote = rank ^ mask;
mask <<= 1;
if (remote >= adjsize) continue;

/* post receive from the remote node */
err = ompi_coll_base_sendrecv_zero(remote, MCA_COLL_BASE_TAG_BARRIER,
remote, MCA_COLL_BASE_TAG_BARRIER,
comm);
if (err != MPI_SUCCESS) { line = __LINE__; goto err_hndl;}
}
}

/* non-power of 2 case */
if (adjsize != size) {
if (rank < (size - adjsize)) {
/* send enter message to higher ranked node */
remote = rank + adjsize;
err = MCA_PML_CALL(send((void*)NULL, 0, MPI_BYTE, remote,
MCA_COLL_BASE_TAG_BARRIER,
MCA_PML_BASE_SEND_SYNCHRONOUS, comm));

if (err != MPI_SUCCESS) { line = __LINE__; goto err_hndl;}
}
}

return MPI_SUCCESS;
}

在不同间隔的进程之间进行交换,真的能实现barrier。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int ompi_coll_base_barrier_intra_bruck(struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int rank, size, distance, to, from, err, line = 0;

size = ompi_comm_size(comm);
if( 1 == size )
return MPI_SUCCESS;
rank = ompi_comm_rank(comm);
OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"ompi_coll_base_barrier_intra_bruck rank %d", rank));

/* exchange data with rank-2^k and rank+2^k */
for (distance = 1; distance < size; distance <<= 1) {
from = (rank + size - distance) % size;
to = (rank + distance) % size;

/* send message to lower ranked node */
err = ompi_coll_base_sendrecv_zero(to, MCA_COLL_BASE_TAG_BARRIER,
from, MCA_COLL_BASE_TAG_BARRIER,
comm);
}

return MPI_SUCCESS;
}

MPI_Bcast

bcast首先检查内存区是否不是空,再调用coll_bcast,同样是函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype,
int root, MPI_Comm comm)
{
/* .... 主要是检查通信域和buffer是否合法,略*/
err = comm->c_coll->coll_bcast(buffer, count, datatype, root, comm,
comm->c_coll->coll_bcast_module);
OMPI_ERRHANDLER_RETURN(err, comm, err, FUNC_NAME);
}

typedef int (*mca_coll_base_module_bcast_init_fn_t)
(void *buff,
int count,
struct ompi_datatype_t *datatype,
int root,
struct ompi_communicator_t *comm,
struct ompi_info_t *info,
ompi_request_t ** request,
struct mca_coll_base_module_2_4_0_t *module);

bcast主要以下几种:bcast相关的算法应该有:0: tuned, 1: binomial, 2: in_order_binomial, 3: binary, 4: pipeline, 5: chain, 6: linear

1
2
3
4
5
int ompi_coll_adapt_bcast(BCAST_ARGS);
调用
int ompi_coll_adapt_ibcast
调用
int ompi_coll_adapt_ibcast_generic

ompi_coll_adapt_ibcast_generic是底层的调用,首先创建temp_request,标明source,tag等。计算要bcast的数据的segment数,有个宏提供了一种计算段的最佳计数的通用方法(即可以适合指定 SEGSIZE 的完整数据类型的数量)。并在堆上给分配空间,以便其他函数访问。如果是根进程,则向所有子进程发送,否则向根进程接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
int ompi_coll_adapt_ibcast_generic(void *buff, int count, struct ompi_datatype_t *datatype, int root,
struct ompi_communicator_t *comm, ompi_request_t ** request,
mca_coll_base_module_t * module, ompi_coll_tree_t * tree,
size_t seg_size)
{
int i, j, rank, err;
/* The min of num_segs and SEND_NUM or RECV_NUM, in case the num_segs is less than SEND_NUM or RECV_NUM */
int min;

/* Number of datatype in a segment */
int seg_count = count;
/* Size of a datatype */
size_t type_size;
/* Real size of a segment */
size_t real_seg_size;
ptrdiff_t extent, lb;
/* Number of segments */
int num_segs;

mca_pml_base_send_mode_t sendmode = (mca_coll_adapt_component.adapt_ibcast_synchronous_send)
? MCA_PML_BASE_SEND_SYNCHRONOUS : MCA_PML_BASE_SEND_STANDARD;

/* The request passed outside */
ompi_coll_base_nbc_request_t *temp_request = NULL;
opal_mutex_t *mutex;
/* Store the segments which are received */
int *recv_array = NULL;
/* Record how many isends have been issued for every child */
int *send_array = NULL;

/* Atomically set up free list */
if (NULL == mca_coll_adapt_component.adapt_ibcast_context_free_list) {
opal_free_list_t* fl = OBJ_NEW(opal_free_list_t);
opal_free_list_init(fl,
sizeof(ompi_coll_adapt_bcast_context_t),
opal_cache_line_size,
OBJ_CLASS(ompi_coll_adapt_bcast_context_t),
0, opal_cache_line_size,
mca_coll_adapt_component.adapt_context_free_list_min,
mca_coll_adapt_component.adapt_context_free_list_max,
mca_coll_adapt_component.adapt_context_free_list_inc,
NULL, 0, NULL, NULL, NULL);
if( !OPAL_ATOMIC_COMPARE_EXCHANGE_STRONG_PTR((opal_atomic_intptr_t *)&mca_coll_adapt_component.adapt_ibcast_context_free_list,
&(intptr_t){0}, fl) ) {
OBJ_RELEASE(fl);
}
}

/* Set up request */
temp_request = OBJ_NEW(ompi_coll_base_nbc_request_t);
OMPI_REQUEST_INIT(&temp_request->super, false);
temp_request->super.req_state = OMPI_REQUEST_ACTIVE;
temp_request->super.req_type = OMPI_REQUEST_COLL;
temp_request->super.req_free = ompi_coll_adapt_request_free;
temp_request->super.req_status.MPI_SOURCE = 0;
temp_request->super.req_status.MPI_TAG = 0;
temp_request->super.req_status.MPI_ERROR = 0;
temp_request->super.req_status._cancelled = 0;
temp_request->super.req_status._ucount = 0;
*request = (ompi_request_t*)temp_request;

/* Set up mutex */
mutex = OBJ_NEW(opal_mutex_t);

rank = ompi_comm_rank(comm);

/* Determine number of elements sent per operation */
ompi_datatype_type_size(datatype, &type_size);
COLL_BASE_COMPUTED_SEGCOUNT(seg_size, type_size, seg_count);

ompi_datatype_get_extent(datatype, &lb, &extent);
num_segs = (count + seg_count - 1) / seg_count;
real_seg_size = (ptrdiff_t) seg_count *extent;

/* Set memory for recv_array and send_array, created on heap becasue they are needed to be accessed by other functions (callback functions) */
if (num_segs != 0) {
recv_array = (int *) malloc(sizeof(int) * num_segs);
}
if (tree->tree_nextsize != 0) {
send_array = (int *) malloc(sizeof(int) * tree->tree_nextsize);
}

/* Set constant context for send and recv call back */
ompi_coll_adapt_constant_bcast_context_t *con = OBJ_NEW(ompi_coll_adapt_constant_bcast_context_t);
con->root = root;
con->count = count;
con->seg_count = seg_count;
con->datatype = datatype;
con->comm = comm;
con->real_seg_size = real_seg_size;
con->num_segs = num_segs;
con->recv_array = recv_array;
con->num_recv_segs = 0;
con->num_recv_fini = 0;
con->send_array = send_array;
con->num_sent_segs = 0;
con->mutex = mutex;
con->request = (ompi_request_t*)temp_request;
con->tree = tree;
con->ibcast_tag = ompi_coll_base_nbc_reserve_tags(comm, num_segs);

OPAL_OUTPUT_VERBOSE((30, mca_coll_adapt_component.adapt_output,
"[%d]: Ibcast, root %d, tag %d\n", rank, root,
con->ibcast_tag));
OPAL_OUTPUT_VERBOSE((30, mca_coll_adapt_component.adapt_output,
"[%d]: con->mutex = %p, num_children = %d, num_segs = %d, real_seg_size = %d, seg_count = %d, tree_adreess = %p\n",
rank, (void *) con->mutex, tree->tree_nextsize, num_segs,
(int) real_seg_size, seg_count, (void *) con->tree));

OPAL_THREAD_LOCK(mutex);

/* If the current process is root, it sends segment to every children */
if (rank == root) {
/* Handle the situation when num_segs < SEND_NUM */
if (num_segs <= mca_coll_adapt_component.adapt_ibcast_max_send_requests) {
min = num_segs;
} else {
min = mca_coll_adapt_component.adapt_ibcast_max_send_requests;
}

/* Set recv_array, root has already had all the segments */
for (i = 0; i < num_segs; i++) {
recv_array[i] = i;
}
con->num_recv_segs = num_segs;
/* Set send_array, will send ompi_coll_adapt_ibcast_max_send_requests segments */
for (i = 0; i < tree->tree_nextsize; i++) {
send_array[i] = mca_coll_adapt_component.adapt_ibcast_max_send_requests;
}

ompi_request_t *send_req;
/* Number of datatypes in each send */
int send_count = seg_count;
for (i = 0; i < min; i++) {
if (i == (num_segs - 1)) {
send_count = count - i * seg_count;
}
for (j = 0; j < tree->tree_nextsize; j++) {
ompi_coll_adapt_bcast_context_t *context =
(ompi_coll_adapt_bcast_context_t *) opal_free_list_wait(mca_coll_adapt_component.
adapt_ibcast_context_free_list);
context->buff = (char *) buff + i * real_seg_size;
context->frag_id = i;
/* The id of peer in in children_list */
context->child_id = j;
/* Actural rank of the peer */
context->peer = tree->tree_next[j];
context->con = con;
OBJ_RETAIN(con);

char *send_buff = context->buff;
OPAL_OUTPUT_VERBOSE((30, mca_coll_adapt_component.adapt_output,
"[%d]: Send(start in main): segment %d to %d at buff %p send_count %d tag %d\n",
rank, context->frag_id, context->peer,
(void *) send_buff, send_count, con->ibcast_tag - i));
err =
MCA_PML_CALL(isend
(send_buff, send_count, datatype, context->peer,
con->ibcast_tag - i, sendmode, comm,
&send_req));
if (MPI_SUCCESS != err) {
return err;
}
/* Set send callback */
OPAL_THREAD_UNLOCK(mutex);
ompi_request_set_callback(send_req, send_cb, context);
OPAL_THREAD_LOCK(mutex);
}
}

}

/* If the current process is not root, it receives data from parent in the tree. */
else {
/* Handle the situation when num_segs < RECV_NUM */
if (num_segs <= mca_coll_adapt_component.adapt_ibcast_max_recv_requests) {
min = num_segs;
} else {
min = mca_coll_adapt_component.adapt_ibcast_max_recv_requests;
}

/* Set recv_array, recv_array is empty */
for (i = 0; i < num_segs; i++) {
recv_array[i] = 0;
}
/* Set send_array to empty */
for (i = 0; i < tree->tree_nextsize; i++) {
send_array[i] = 0;
}

/* Create a recv request */
ompi_request_t *recv_req;

/* Recevice some segments from its parent */
int recv_count = seg_count;
for (i = 0; i < min; i++) {
if (i == (num_segs - 1)) {
recv_count = count - i * seg_count;
}
ompi_coll_adapt_bcast_context_t *context =
(ompi_coll_adapt_bcast_context_t *) opal_free_list_wait(mca_coll_adapt_component.
adapt_ibcast_context_free_list);
context->buff = (char *) buff + i * real_seg_size;
context->frag_id = i;
context->peer = tree->tree_prev;
context->con = con;
OBJ_RETAIN(con);
char *recv_buff = context->buff;
OPAL_OUTPUT_VERBOSE((30, mca_coll_adapt_component.adapt_output,
"[%d]: Recv(start in main): segment %d from %d at buff %p recv_count %d tag %d\n",
ompi_comm_rank(context->con->comm), context->frag_id,
context->peer, (void *) recv_buff, recv_count,
con->ibcast_tag - i));
err =
MCA_PML_CALL(irecv
(recv_buff, recv_count, datatype, context->peer,
con->ibcast_tag - i, comm, &recv_req));
if (MPI_SUCCESS != err) {
return err;
}
/* Set receive callback */
OPAL_THREAD_UNLOCK(mutex);
ompi_request_set_callback(recv_req, recv_cb, context);
OPAL_THREAD_LOCK(mutex);
}

}

OPAL_THREAD_UNLOCK(mutex);

OPAL_OUTPUT_VERBOSE((30, mca_coll_adapt_component.adapt_output,
"[%d]: End of Ibcast\n", rank));

return MPI_SUCCESS;
}

此外还找到了如下几个:

  • ompi_coll_base_bcast_intra_basic_linear:root发送给所有其他进程
  • mca_coll_basic_bcast_log_intra:log复杂度的树形通信
  • ompi_coll_base_bcast_intra_generic:树形发送,根节点发送给中间节点,中间节点从根节点中接收,再发送给自己的子节点,叶子节点只负责接收
  • mca_coll_sm_bcast_intra:共享内存的bcast
    • 找到标志,memcpy,子进程感觉到完成了,再发送给子子进程
    • 对于根,一般算法是等待一组段变得可用。一旦它可用,根通过将当前操作号和使用该集合的进程数写入标志来声明该集合。
    • 然后根在这组段上循环;对于每个段,它将用户缓冲区的一个片段复制到共享数据段中,然后将数据大小写入其子控制缓冲区。
    • 重复该过程,直到已写入所有片段。
    • 对于非根,对于每组缓冲区,它们等待直到当前操作号出现在使用标志中(即,由根写入)。
    • 然后对于每个段,它们等待一个非零值出现在它们的控制缓冲区中。如果他们有孩子,他们将数据从他们父母的共享数据段复制到他们的共享数据段,并将数据大小写入他们的每个孩子的控制缓冲区。
    • 然后,他们将共享的数据段中的数据复制到用户的输出缓冲区中。
    • 重复该过程,直到已接收到所有片段。如果他们没有孩子,他们直接将数据从父母的共享数据段复制到用户的输出缓冲区。
  • mca_coll_sync_bcast
    • 加上了一些barrier
  • ompi_coll_tuned_bcast_intra_dec_fixed
    • 根据消息大小,进程数选择算法执行bcast
  • ompi_coll_base_bcast_intra_bintree:跟ompi_coll_base_bcast_intra_generic一样,树不一样
  • ompi_coll_base_bcast_intra_binomial:跟ompi_coll_base_bcast_intra_generic一样
  • ompi_coll_base_bcast_intra_knomial:树的子节点数不同,如果radix=2,子节点有1,2,4,8;radix=3,子节点有3,6,9这样。

ompi_coll_base_bcast_intra_scatter_allgather:借助allgather实现bcast,例如,0和1一组,2和3一组,4和5一组,6和7一组,这样第一次就能实现每个进程里两个数据,第二次就是0,1,2,3一组,4,5,6,7一组,每个进程里4个,最后一次就每个进程里8个了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* Time complexity: O(\alpha\log(p) + \beta*m((p-1)/p))
* Binomial tree scatter: \alpha\log(p) + \beta*m((p-1)/p)
* Recursive doubling allgather: \alpha\log(p) + \beta*m((p-1)/p)
*
* Example, p=8, count=8, root=0
* Binomial tree scatter Recursive doubling allgather
* 0: --+ --+ --+ [0*******] <-+ [01******] <--+ [0123****] <--+
* 1: | 2| <-+ [*1******] <-+ [01******] <--|-+ [0123****] <--+-+
* 2: 4| <-+ --+ [**2*****] <-+ [**23****] <--+ | [0123****] <--+-+-+
* 3: | <-+ [***3****] <-+ [**23****] <----+ [0123****] <--+-+-+-+
* 4: <-+ --+ --+ [****4***] <-+ [****45**] <--+ [****4567] <--+ | | |
* 5: 2| <-+ [*****5**] <-+ [****45**] <--|-+ [****4567] <----+ | |
* 6: <-+ --+ [******6*] <-+ [******67] <--+ | [****4567] <------+ |
* 7: <-+ [*******7] <-+ [******67] <--|-+ [****4567] <--------+
*/
int ompi_coll_base_bcast_intra_scatter_allgather(
void *buf, int count, struct ompi_datatype_t *datatype, int root,
struct ompi_communicator_t *comm, mca_coll_base_module_t *module,
uint32_t segsize)
{
int err = MPI_SUCCESS;
ptrdiff_t lb, extent;
size_t datatype_size;
MPI_Status status;
ompi_datatype_get_extent(datatype, &lb, &extent);
ompi_datatype_type_size(datatype, &datatype_size);
int comm_size = ompi_comm_size(comm);
int rank = ompi_comm_rank(comm);

int vrank = (rank - root + comm_size) % comm_size;
int recv_count = 0, send_count = 0;
int scatter_count = (count + comm_size - 1) / comm_size; /* ceil(count / comm_size) */
int curr_count = (rank == root) ? count : 0;

/* Scatter by binomial tree: receive data from parent */
int mask = 0x1;
while (mask < comm_size) {
if (vrank & mask) {
int parent = (rank - mask + comm_size) % comm_size;
/* Compute an upper bound on recv block size */
recv_count = count - vrank * scatter_count;
if (recv_count <= 0) {
curr_count = 0;
} else {
/* Recv data from parent */
err = MCA_PML_CALL(recv((char *)buf + (ptrdiff_t)vrank * scatter_count * extent,
recv_count, datatype, parent,
MCA_COLL_BASE_TAG_BCAST, comm, &status));
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
/* Get received count */
curr_count = (int)(status._ucount / datatype_size);
}
break;
}
mask <<= 1;
}

/* Scatter by binomial tree: send data to child processes */
mask >>= 1;
while (mask > 0) {
if (vrank + mask < comm_size) {
send_count = curr_count - scatter_count * mask;
if (send_count > 0) {
int child = (rank + mask) % comm_size;
err = MCA_PML_CALL(send((char *)buf + (ptrdiff_t)scatter_count * (vrank + mask) * extent,
send_count, datatype, child,
MCA_COLL_BASE_TAG_BCAST,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
curr_count -= send_count;
}
}
mask >>= 1;
}

/*
* Allgather by recursive doubling
* Each process has the curr_count elems in the buf[vrank * scatter_count, ...]
*/
int rem_count = count - vrank * scatter_count;
curr_count = (scatter_count < rem_count) ? scatter_count : rem_count;
if (curr_count < 0)
curr_count = 0;

mask = 0x1;
while (mask < comm_size) {
int vremote = vrank ^ mask;
int remote = (vremote + root) % comm_size;

int vrank_tree_root = ompi_rounddown(vrank, mask);
int vremote_tree_root = ompi_rounddown(vremote, mask);

if (vremote < comm_size) {
ptrdiff_t send_offset = vrank_tree_root * scatter_count * extent;
ptrdiff_t recv_offset = vremote_tree_root * scatter_count * extent;
recv_count = count - vremote_tree_root * scatter_count;
if (recv_count < 0)
recv_count = 0;
err = ompi_coll_base_sendrecv((char *)buf + send_offset,
curr_count, datatype, remote,
MCA_COLL_BASE_TAG_BCAST,
(char *)buf + recv_offset,
recv_count, datatype, remote,
MCA_COLL_BASE_TAG_BCAST,
comm, &status, rank);
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
recv_count = (int)(status._ucount / datatype_size);
curr_count += recv_count;
}

/*
* Non-power-of-two case: if process did not have destination process
* to communicate with, we need to send him the current result.
* Recursive halving algorithm is used for search of process.
*/
if (vremote_tree_root + mask > comm_size) {
int nprocs_alldata = comm_size - vrank_tree_root - mask;
int offset = scatter_count * (vrank_tree_root + mask);
for (int rhalving_mask = mask >> 1; rhalving_mask > 0; rhalving_mask >>= 1) {
vremote = vrank ^ rhalving_mask;
remote = (vremote + root) % comm_size;
int tree_root = ompi_rounddown(vrank, rhalving_mask << 1);
/*
* Send only if:
* 1) current process has data: (vremote > vrank) && (vrank < tree_root + nprocs_alldata)
* 2) remote process does not have data at any step: vremote >= tree_root + nprocs_alldata
*/
if ((vremote > vrank) && (vrank < tree_root + nprocs_alldata)
&& (vremote >= tree_root + nprocs_alldata)) {
err = MCA_PML_CALL(send((char *)buf + (ptrdiff_t)offset * extent,
recv_count, datatype, remote,
MCA_COLL_BASE_TAG_BCAST,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) { goto cleanup_and_return; }

} else if ((vremote < vrank) && (vremote < tree_root + nprocs_alldata)
&& (vrank >= tree_root + nprocs_alldata)) {
err = MCA_PML_CALL(recv((char *)buf + (ptrdiff_t)offset * extent,
count, datatype, remote,
MCA_COLL_BASE_TAG_BCAST,
comm, &status));
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
recv_count = (int)(status._ucount / datatype_size);
curr_count += recv_count;
}
}
}
mask <<= 1;
}

cleanup_and_return:
return err;
}

ompi_coll_base_bcast_intra_scatter_allgather_ring:跟上边的一样,不过每个进程都是跟之前的进程交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/*
* Time complexity: O(\alpha(\log(p) + p) + \beta*m((p-1)/p))
* Binomial tree scatter: \alpha\log(p) + \beta*m((p-1)/p)
* Ring allgather: 2(p-1)(\alpha + m/p\beta)
*
* Example, p=8, count=8, root=0
* Binomial tree scatter Ring allgather: p - 1 steps
* 0: --+ --+ --+ [0*******] [0******7] [0*****67] [0****567] ... [01234567]
* 1: | 2| <-+ [*1******] [01******] [01*****7] [01****67] ... [01234567]
* 2: 4| <-+ --+ [**2*****] [*12*****] [012*****] [012****7] ... [01234567]
* 3: | <-+ [***3****] [**23****] [*123****] [0123****] ... [01234567]
* 4: <-+ --+ --+ [****4***] [***34***] [**234***] [*1234***] ... [01234567]
* 5: 2| <-+ [*****5**] [****45**] [***345**] [**2345**] ... [01234567]
* 6: <-+ --+ [******6*] [*****56*] [****456*] [***3456*] ... [01234567]
* 7: <-+ [*******7] [******67] [*****567] [****4567] ... [01234567]
*/
int ompi_coll_base_bcast_intra_scatter_allgather_ring(
void *buf, int count, struct ompi_datatype_t *datatype, int root,
struct ompi_communicator_t *comm, mca_coll_base_module_t *module,
uint32_t segsize)
{
int err = MPI_SUCCESS;
ptrdiff_t lb, extent;
size_t datatype_size;
MPI_Status status;
ompi_datatype_get_extent(datatype, &lb, &extent);
ompi_datatype_type_size(datatype, &datatype_size);
int comm_size = ompi_comm_size(comm);
int rank = ompi_comm_rank(comm);

int vrank = (rank - root + comm_size) % comm_size;
int recv_count = 0, send_count = 0;
int scatter_count = (count + comm_size - 1) / comm_size; /* ceil(count / comm_size) */
int curr_count = (rank == root) ? count : 0;

/* Scatter by binomial tree: receive data from parent */
int mask = 1;
while (mask < comm_size) {
if (vrank & mask) {
int parent = (rank - mask + comm_size) % comm_size;
/* Compute an upper bound on recv block size */
recv_count = count - vrank * scatter_count;
if (recv_count <= 0) {
curr_count = 0;
} else {
/* Recv data from parent */
err = MCA_PML_CALL(recv((char *)buf + (ptrdiff_t)vrank * scatter_count * extent,
recv_count, datatype, parent,
MCA_COLL_BASE_TAG_BCAST, comm, &status));
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
/* Get received count */
curr_count = (int)(status._ucount / datatype_size);
}
break;
}
mask <<= 1;
}

/* Scatter by binomial tree: send data to child processes */
mask >>= 1;
while (mask > 0) {
if (vrank + mask < comm_size) {
send_count = curr_count - scatter_count * mask;
if (send_count > 0) {
int child = (rank + mask) % comm_size;
err = MCA_PML_CALL(send((char *)buf + (ptrdiff_t)scatter_count * (vrank + mask) * extent,
send_count, datatype, child,
MCA_COLL_BASE_TAG_BCAST,
MCA_PML_BASE_SEND_STANDARD, comm));
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
curr_count -= send_count;
}
}
mask >>= 1;
}

/* Allgather by a ring algorithm */
int left = (rank - 1 + comm_size) % comm_size;
int right = (rank + 1) % comm_size;
int send_block = vrank;
int recv_block = (vrank - 1 + comm_size) % comm_size;

for (int i = 1; i < comm_size; i++) {
recv_count = (scatter_count < count - recv_block * scatter_count) ?
scatter_count : count - recv_block * scatter_count;
if (recv_count < 0)
recv_count = 0;
ptrdiff_t recv_offset = recv_block * scatter_count * extent;

send_count = (scatter_count < count - send_block * scatter_count) ?
scatter_count : count - send_block * scatter_count;
if (send_count < 0)
send_count = 0;
ptrdiff_t send_offset = send_block * scatter_count * extent;

err = ompi_coll_base_sendrecv((char *)buf + send_offset, send_count,
datatype, right, MCA_COLL_BASE_TAG_BCAST,
(char *)buf + recv_offset, recv_count,
datatype, left, MCA_COLL_BASE_TAG_BCAST,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { goto cleanup_and_return; }
send_block = recv_block;
recv_block = (recv_block - 1 + comm_size) % comm_size;
}

cleanup_and_return:
return err;
}

MPI_Send

经过了一系列错误检查之后,主要是mca_pml.pml_send这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int MPI_Send(const void *buf, int count, MPI_Datatype type, int dest,
int tag, MPI_Comm comm)
{
int rc = MPI_SUCCESS;

SPC_RECORD(OMPI_SPC_SEND, 1);

MEMCHECKER(
memchecker_datatype(type);
memchecker_call(&opal_memchecker_base_isdefined, buf, count, type);
memchecker_comm(comm);
);

if ( MPI_PARAM_CHECK ) {
OMPI_ERR_INIT_FINALIZE(FUNC_NAME);
if (ompi_comm_invalid(comm)) {
return OMPI_ERRHANDLER_NOHANDLE_INVOKE(MPI_ERR_COMM, FUNC_NAME);
} else if (count < 0) {
rc = MPI_ERR_COUNT;
} else if (tag < 0 || tag > mca_pml.pml_max_tag) {
rc = MPI_ERR_TAG;
} else if (ompi_comm_peer_invalid(comm, dest) &&
(MPI_PROC_NULL != dest)) {
rc = MPI_ERR_RANK;
} else {
OMPI_CHECK_DATATYPE_FOR_SEND(rc, type, count);
OMPI_CHECK_USER_BUFFER(rc, buf, type, count);
}
OMPI_ERRHANDLER_CHECK(rc, comm, rc, FUNC_NAME);
}

#if OPAL_ENABLE_FT_MPI
/*
* An early check, so as to return early if we are communicating with
* a failed process. This is not absolutely necessary since we will
* check for this, and other, error conditions during the completion
* call in the PML.
*/
if( OPAL_UNLIKELY(!ompi_comm_iface_p2p_check_proc(comm, dest, &rc)) ) {
OMPI_ERRHANDLER_RETURN(rc, comm, rc, FUNC_NAME);
}
#endif

if (MPI_PROC_NULL == dest) {
return MPI_SUCCESS;
}

rc = MCA_PML_CALL(send(buf, count, type, dest, tag, MCA_PML_BASE_SEND_STANDARD, comm));
OMPI_ERRHANDLER_RETURN(rc, comm, rc, FUNC_NAME);
}

pml_send函数主要有以下几个赋值:

1
2
3
4
5
mca_pml_cm_send
mca_pml_monitoring_send
mca_pml_ob1_send
mca_pml_ucx_send
mca_spml_ucx_send

以第一个为例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
__opal_attribute_always_inline__ static inline int
mca_pml_cm_send(const void *buf,
size_t count,
ompi_datatype_t* datatype,
int dst,
int tag,
mca_pml_base_send_mode_t sendmode,
ompi_communicator_t* comm)
{
int ret = OMPI_ERROR;
uint32_t flags = 0;
ompi_proc_t * ompi_proc;

if(sendmode == MCA_PML_BASE_SEND_BUFFERED) {
mca_pml_cm_hvy_send_request_t *sendreq;

MCA_PML_CM_HVY_SEND_REQUEST_ALLOC(sendreq, comm, dst, ompi_proc);
if (OPAL_UNLIKELY(NULL == sendreq)) return OMPI_ERR_OUT_OF_RESOURCE;

MCA_PML_CM_HVY_SEND_REQUEST_INIT(sendreq, ompi_proc, comm, tag, dst, datatype, sendmode, false, false, buf, count, flags);
MCA_PML_CM_HVY_SEND_REQUEST_START(sendreq, ret);
if (OPAL_UNLIKELY(OMPI_SUCCESS != ret)) {
MCA_PML_CM_HVY_SEND_REQUEST_RETURN(sendreq);
return ret;
}

ompi_request_free( (ompi_request_t**)&sendreq );
} else {
opal_convertor_t convertor;
OBJ_CONSTRUCT(&convertor, opal_convertor_t);
#if !(OPAL_ENABLE_HETEROGENEOUS_SUPPORT)
if (opal_datatype_is_contiguous_memory_layout(&datatype->super, count)) {

convertor.remoteArch = ompi_mpi_local_convertor->remoteArch;
convertor.flags = ompi_mpi_local_convertor->flags;
convertor.master = ompi_mpi_local_convertor->master;

convertor.local_size = count * datatype->super.size;
convertor.pBaseBuf = (unsigned char*)buf + datatype->super.true_lb;
convertor.count = count;
convertor.pDesc = &datatype->super;

#if OPAL_CUDA_SUPPORT
/* Switches off CUDA detection if
MTL set MCA_MTL_BASE_FLAG_CUDA_INIT_DISABLE during init */
MCA_PML_CM_SWITCH_CUDA_CONVERTOR_OFF(flags, datatype, count);
convertor.flags |= flags;
/* Sets CONVERTOR_CUDA flag if CUDA buffer */
opal_convertor_prepare_for_send( &convertor, &datatype->super, count, buf );
#endif
} else
#endif
{
ompi_proc = ompi_comm_peer_lookup(comm, dst);

MCA_PML_CM_SWITCH_CUDA_CONVERTOR_OFF(flags, datatype, count);

opal_convertor_copy_and_prepare_for_send(
ompi_proc->super.proc_convertor,
&datatype->super, count, buf, flags,
&convertor);
}

ret = OMPI_MTL_CALL(send(ompi_mtl,
comm,
dst,
tag,
&convertor,
sendmode));
OBJ_DESTRUCT(&convertor);
}

return ret;
}

因为这是简单的send,所以分为两种情况,第一种是有buffer,先分配request,初始化之后等待返回。MCA_PML_CM_HVY_SEND_REQUEST_ALLOC是分配一个request,request应该是opal_free_list_wait(只包括了有多线程情况下的opal_free_list_wait_mt(fl);和无多线程情况下的opal_free_list_wait_st(fl)的调用)函数分配的,并规定了完成后的回调函数mca_pml_cm_send_request_completion

1
2
3
4
5
6
7
8
9
10
#define MCA_PML_CM_HVY_SEND_REQUEST_ALLOC(sendreq, comm, dst,           \
ompi_proc) \
{ \
sendreq = (mca_pml_cm_hvy_send_request_t*) \
opal_free_list_wait (&mca_pml_base_send_requests); \
sendreq->req_send.req_base.req_pml_type = MCA_PML_CM_REQUEST_SEND_HEAVY; \
sendreq->req_mtl.ompi_req = (ompi_request_t*) sendreq; \
sendreq->req_mtl.completion_callback = mca_pml_cm_send_request_completion; \
}
#endif

从一个栈结构里取出来一个proc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static inline opal_free_list_item_t *opal_free_list_wait_st(opal_free_list_t *fl)
{
opal_free_list_item_t *item = (opal_free_list_item_t *) opal_lifo_pop(&fl->super);

while (NULL == item) {
if (fl->fl_max_to_alloc <= fl->fl_num_allocated
|| OPAL_SUCCESS != opal_free_list_grow_st(fl, fl->fl_num_per_alloc, &item)) {
/* try to make progress */
opal_progress();
}
if (NULL == item) {
item = (opal_free_list_item_t *) opal_lifo_pop(&fl->super);
}
}

return item;
}

/**
* Blocking call to obtain an item from a free list.
*/
static inline opal_free_list_item_t *opal_free_list_wait_mt(opal_free_list_t *fl)
{
opal_free_list_item_t *item = (opal_free_list_item_t *) opal_lifo_pop_atomic(&fl->super);

while (NULL == item) {
if (!opal_mutex_trylock(&fl->fl_lock)) {
if (fl->fl_max_to_alloc <= fl->fl_num_allocated
|| OPAL_SUCCESS != opal_free_list_grow_st(fl, fl->fl_num_per_alloc, &item)) {
fl->fl_num_waiting++;
opal_condition_wait(&fl->fl_condition, &fl->fl_lock);
fl->fl_num_waiting--;
} else {
if (0 < fl->fl_num_waiting) {
if (1 == fl->fl_num_waiting) {
opal_condition_signal(&fl->fl_condition);
} else {
opal_condition_broadcast(&fl->fl_condition);
}
}
}
} else {
/* If I wasn't able to get the lock in the begining when I finaly grab it
* the one holding the lock in the begining already grow the list. I will
* release the lock and try to get a new element until I succeed.
*/
opal_mutex_lock(&fl->fl_lock);
}
opal_mutex_unlock(&fl->fl_lock);
if (NULL == item) {
item = (opal_free_list_item_t *) opal_lifo_pop_atomic(&fl->super);
}
}

return item;
}

回调函数mca_pml_cm_send_request_completion,主要是为了调用MCA_PML_CM_THIN_SEND_REQUEST_PML_COMPLETE的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void
mca_pml_cm_send_request_completion(struct mca_mtl_request_t *mtl_request)
{
mca_pml_cm_send_request_t *base_request =
(mca_pml_cm_send_request_t*) mtl_request->ompi_req;
if( MCA_PML_CM_REQUEST_SEND_THIN == base_request->req_base.req_pml_type ) {
MCA_PML_CM_THIN_SEND_REQUEST_PML_COMPLETE(((mca_pml_cm_thin_send_request_t*) base_request));
} else {
MCA_PML_CM_HVY_SEND_REQUEST_PML_COMPLETE(((mca_pml_cm_hvy_send_request_t*) base_request));
}
}

/*
* The PML has completed a send request. Note that this request
* may have been orphaned by the user or have already completed
* at the MPI level.
* This macro will never be called directly from the upper level, as it should
* only be an internal call to the PML.
*/
#define MCA_PML_CM_THIN_SEND_REQUEST_PML_COMPLETE(sendreq) \
do { \
assert( false == sendreq->req_send.req_base.req_pml_complete ); \
\
if( !REQUEST_COMPLETE(&sendreq->req_send.req_base.req_ompi)) { \
/* Should only be called for long messages (maybe synchronous) */ \
ompi_request_complete(&(sendreq->req_send.req_base.req_ompi), true); \
} \
sendreq->req_send.req_base.req_pml_complete = true; \
\
if( sendreq->req_send.req_base.req_free_called ) { \
MCA_PML_CM_THIN_SEND_REQUEST_RETURN( sendreq ); \
} \
} while (0)

/*
* The PML has completed a send request. Note that this request
* may have been orphaned by the user or have already completed
* at the MPI level.
* This macro will never be called directly from the upper level, as it should
* only be an internal call to the PML.
*/
#define MCA_PML_CM_HVY_SEND_REQUEST_PML_COMPLETE(sendreq) \
do { \
assert( false == sendreq->req_send.req_base.req_pml_complete ); \
\
if (sendreq->req_send.req_send_mode == MCA_PML_BASE_SEND_BUFFERED && \
sendreq->req_count > 0 ) { \
mca_pml_base_bsend_request_free(sendreq->req_buff); \
} \
\
if( !REQUEST_COMPLETE(&sendreq->req_send.req_base.req_ompi)) { \
/* the request may have already been marked complete by the MTL */ \
ompi_request_complete(&(sendreq->req_send.req_base.req_ompi), true); \
} \
sendreq->req_send.req_base.req_pml_complete = true; \
\
if( sendreq->req_send.req_base.req_free_called ) { \
MCA_PML_CM_HVY_SEND_REQUEST_RETURN( sendreq ); \
} else { \
if(sendreq->req_send.req_base.req_ompi.req_persistent) { \
/* rewind convertor */ \
size_t offset = 0; \
opal_convertor_set_position(&sendreq->req_send.req_base.req_convertor, \
&offset); \
} \
} \
} while (0)

分配完之后调用MCA_PML_CM_HVY_SEND_REQUEST_INIT进行初始化,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

#define MCA_PML_CM_HVY_SEND_REQUEST_INIT( sendreq, \
ompi_proc, \
comm, \
tag, \
dst, \
datatype, \
sendmode, \
persistent, \
blocking, \
buf, \
count, \
flags ) \
do { \
OMPI_REQUEST_INIT(&(sendreq->req_send.req_base.req_ompi), \
persistent); \
sendreq->req_tag = tag; \
sendreq->req_peer = dst; \
sendreq->req_addr = buf; \
sendreq->req_count = count; \
MCA_PML_CM_HVY_SEND_REQUEST_INIT_COMMON( (&sendreq->req_send), \
ompi_proc, \
comm, \
tag, \
datatype, \
sendmode, \
buf, \
count, \
flags ) \
opal_convertor_get_packed_size( \
&sendreq->req_send.req_base.req_convertor, \
&sendreq->req_count ); \
\
sendreq->req_blocking = blocking; \
sendreq->req_send.req_base.req_pml_complete = \
(persistent ? true:false); \
} while(0)


#define MCA_PML_CM_THIN_SEND_REQUEST_INIT( sendreq, \
ompi_proc, \
comm, \
tag, \
dst, \
datatype, \
sendmode, \
buf, \
count, \
flags ) \
do { \
OMPI_REQUEST_INIT(&(sendreq->req_send.req_base.req_ompi), \
false); \
MCA_PML_CM_SEND_REQUEST_INIT_COMMON( (&sendreq->req_send), \
ompi_proc, \
comm, \
tag, \
datatype, \
sendmode, \
buf, \
count, \
flags); \
sendreq->req_send.req_base.req_pml_complete = false; \
} while(0)

初始化完成之后开始执行send-request,并释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

#define MCA_PML_CM_HVY_SEND_REQUEST_START(sendreq, ret) \
do { \
ret = OMPI_SUCCESS; \
MCA_PML_CM_SEND_REQUEST_START_SETUP(&(sendreq)->req_send); \
if (sendreq->req_send.req_send_mode == MCA_PML_BASE_SEND_BUFFERED) { \
MCA_PML_CM_HVY_SEND_REQUEST_BSEND_ALLOC(sendreq, ret); \
} \
if (OMPI_SUCCESS == ret) { \
ret = OMPI_MTL_CALL(isend(ompi_mtl, \
sendreq->req_send.req_base.req_comm, \
sendreq->req_peer, \
sendreq->req_tag, \
&sendreq->req_send.req_base.req_convertor, \
sendreq->req_send.req_send_mode, \
sendreq->req_blocking, \
&sendreq->req_mtl)); \
if(OMPI_SUCCESS == ret && \
sendreq->req_send.req_send_mode == MCA_PML_BASE_SEND_BUFFERED) { \
sendreq->req_send.req_base.req_ompi.req_status.MPI_ERROR = 0; \
if(!REQUEST_COMPLETE(&sendreq->req_send.req_base.req_ompi)) { \
/* request may have already been marked complete by the MTL */ \
ompi_request_complete(&(sendreq)->req_send.req_base.req_ompi, true); \
} \
} \
} \
} while (0)

否则,如果不是buffer类型的send,首先创建一个convertor(后边看,可能是在不同架构下进行通信的转换器),如果没有异构的支持,需要考虑传输的数据是不是连续的,支持异构的话就不需要额外考虑内存连续性。OPAL_CUDA_SUPPORT考虑了cuda的特点。

ompi_comm_peer_lookup用于找到通信对方进程ompi_proc_t结构,原来找一个对方通信进程还需要加锁。它最终是调用了这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @brief Helper function for retreiving the proc of a group member in a dense group
*
* This function exists to handle the translation of sentinel group members to real
* ompi_proc_t's. If a sentinel value is found and allocate is true then this function
* looks for an existing ompi_proc_t using ompi_proc_for_name which will allocate a
* ompi_proc_t if one does not exist. If allocate is false then sentinel values translate
* to NULL.
*/
static inline struct ompi_proc_t *ompi_group_dense_lookup (ompi_group_t *group, const int peer_id, const bool allocate)
{
ompi_proc_t *proc;

proc = group->grp_proc_pointers[peer_id];

if (OPAL_UNLIKELY(ompi_proc_is_sentinel (proc))) {
if (!allocate) {
return NULL;
}

/* replace sentinel value with an actual ompi_proc_t */
ompi_proc_t *real_proc =
(ompi_proc_t *) ompi_proc_for_name (ompi_proc_sentinel_to_name ((uintptr_t) proc));
// 在hash table里找proc

if (opal_atomic_compare_exchange_strong_ptr ((opal_atomic_intptr_t *)(group->grp_proc_pointers + peer_id),
(intptr_t *) &proc, (intptr_t) real_proc)) {
OBJ_RETAIN(real_proc);
}

proc = real_proc;
}

return proc;
}

然后这样就可以调用ompi_mtl->mtl_send,主要是这个函数ompi_mtl_psm2_send,到了MTL层。send还有一种实现是调用了fabric库的操作,这个先不看了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
int
ompi_mtl_psm2_send(struct mca_mtl_base_module_t* mtl,
struct ompi_communicator_t* comm,
int dest,
int tag,
struct opal_convertor_t *convertor,
mca_pml_base_send_mode_t mode)
{
psm2_error_t err;
mca_mtl_psm2_request_t mtl_psm2_request;
psm2_mq_tag_t mqtag;
uint32_t flags = 0;
int ret;
size_t length;
ompi_proc_t* ompi_proc = ompi_comm_peer_lookup( comm, dest );
mca_mtl_psm2_endpoint_t* psm2_endpoint = ompi_mtl_psm2_get_endpoint (mtl, ompi_proc);

assert(mtl == &ompi_mtl_psm2.super);

PSM2_MAKE_MQTAG(comm->c_index, comm->c_my_rank, tag, mqtag);

ret = ompi_mtl_datatype_pack(convertor,
&mtl_psm2_request.buf,
&length,
&mtl_psm2_request.free_after);

if (length >= 1ULL << sizeof(uint32_t) * 8) {
opal_show_help("help-mtl-psm2.txt",
"message too big", false,
length, 1ULL << sizeof(uint32_t) * 8);
return OMPI_ERROR;
}
// 前边是pack
mtl_psm2_request.length = length;
mtl_psm2_request.convertor = convertor;
mtl_psm2_request.type = OMPI_mtl_psm2_ISEND;

if (OMPI_SUCCESS != ret) return ret;

if (mode == MCA_PML_BASE_SEND_SYNCHRONOUS)
flags |= PSM2_MQ_FLAG_SENDSYNC;

err = psm2_mq_send2(ompi_mtl_psm2.mq,
psm2_endpoint->peer_addr,
flags,
&mqtag,
mtl_psm2_request.buf,
length);

if (mtl_psm2_request.free_after) {
free(mtl_psm2_request.buf);
}

return err == PSM2_OK ? OMPI_SUCCESS : OMPI_ERROR;
}

到了这里就没法继续追了,psm2_mq_send2是Performance Scaled Messaging 2里的函数,

1
2
psm2_error_t psm2_mq_send2 (psm2_mq_t mq, psm2_epaddr_t dest,
uint32_t flags, psm2_mq_tag_t *stag, const void *buf, uint32_t len)

发送阻塞 MQ 消息。 发送阻塞 MQ 消息的函数,该消息在本地完成,并且可以在返回时修改源数据。

Parameters:

  • mq: Matched Queue handle.
  • dest: Destination EP address.
  • flags: Message flags, currently:
    • PSM2_MQ_FLAG_SENDSYNC tells PSM2 to send the message synchronously, meaning that the message is not sent until the receiver acknowledges that it has matched the send with a receive buffer.
  • stag: Message Send Tag pointer.
  • buf: Source buffer pointer.
  • len: Length of message starting at buf.

TCP

看代码里有tcp和rdma的实现,但是没找到怎么到tcp这块的,看到注释说是动态加载模块,可能是通过配置实现选择TCP或者RDMA的?以下缕一下TCP的执行过程。

btl_tcp_component.c开始,这个结构保存了网络通信的信息,同时支持IPv4和IPv6,可以看到TCP通信时数据是以帧frag为单位的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct mca_btl_tcp_component_t {
mca_btl_base_component_3_0_0_t super; /**< base BTL component */
uint32_t tcp_addr_count; /**< total number of addresses */
uint32_t tcp_num_btls; /**< number of interfaces available to the TCP component */
unsigned int tcp_num_links; /**< number of logical links per physical device */
struct mca_btl_tcp_module_t **tcp_btls; /**< array of available BTL modules */
opal_list_t local_ifs; /**< opal list of local opal_if_t interfaces */
int tcp_free_list_num; /**< initial size of free lists */
int tcp_free_list_max; /**< maximum size of free lists */
int tcp_free_list_inc; /**< number of elements to alloc when growing free lists */
int tcp_endpoint_cache; /**< amount of cache on each endpoint */
opal_proc_table_t tcp_procs; /**< hash table of tcp proc structures */
opal_mutex_t tcp_lock; /**< lock for accessing module state */
opal_list_t tcp_events;

opal_event_t tcp_recv_event; /**< recv event for IPv4 listen socket */
int tcp_listen_sd; /**< IPv4 listen socket for incoming connection requests */
unsigned short tcp_listen_port; /**< IPv4 listen port */
int tcp_port_min; /**< IPv4 minimum port */
int tcp_port_range; /**< IPv4 port range */
#if OPAL_ENABLE_IPV6
opal_event_t tcp6_recv_event; /**< recv event for IPv6 listen socket */
int tcp6_listen_sd; /**< IPv6 listen socket for incoming connection requests */
unsigned short tcp6_listen_port; /**< IPv6 listen port */
int tcp6_port_min; /**< IPv4 minimum port */
int tcp6_port_range; /**< IPv4 port range */
#endif
/* Port range restriction */

char *tcp_if_include; /**< comma seperated list of interface to include */
char *tcp_if_exclude; /**< comma seperated list of interface to exclude */
int tcp_sndbuf; /**< socket sndbuf size */
int tcp_rcvbuf; /**< socket rcvbuf size */
int tcp_disable_family; /**< disabled AF_family */

/* free list of fragment descriptors */
opal_free_list_t tcp_frag_eager;
opal_free_list_t tcp_frag_max;
opal_free_list_t tcp_frag_user;

int tcp_enable_progress_thread; /** Support for tcp progress thread flag */

opal_event_t tcp_recv_thread_async_event;
opal_mutex_t tcp_frag_eager_mutex;
opal_mutex_t tcp_frag_max_mutex;
opal_mutex_t tcp_frag_user_mutex;
/* Do we want to use TCP_NODELAY? */
int tcp_not_use_nodelay;

/* do we want to warn on all excluded interfaces
* that are not found?
*/
bool report_all_unfound_interfaces;
};

mca_btl_tcp_module_t是一个中间层,保存了tcp通信的每步需要调用的函数指针。以mca_btl_tcp_send为例记录调用历程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mca_btl_tcp_module_t mca_btl_tcp_module =
{.super =
{
.btl_component = &mca_btl_tcp_component.super,
.btl_add_procs = mca_btl_tcp_add_procs,
.btl_del_procs = mca_btl_tcp_del_procs,
.btl_finalize = mca_btl_tcp_finalize,
.btl_alloc = mca_btl_tcp_alloc,
.btl_free = mca_btl_tcp_free,
.btl_prepare_src = mca_btl_tcp_prepare_src,
.btl_send = mca_btl_tcp_send,
.btl_put = mca_btl_tcp_put,
.btl_dump = mca_btl_base_dump,
.btl_register_error = mca_btl_tcp_register_error_cb, /* register error */
},
.tcp_endpoints_mutex = OPAL_MUTEX_STATIC_INIT};

mca_btl_tcp_send首先开启一个异步的发送过程,新建一个fragment,记录下每一个segment。put和get都是跟它差不多,最终都是调用的mca_btl_tcp_endpoint_send

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Initiate an asynchronous send.
*
* @param btl (IN) BTL module
* @param endpoint (IN) BTL addressing information
* @param descriptor (IN) Description of the data to be transfered
* @param tag (IN) The tag value used to notify the peer.
*/

int mca_btl_tcp_send(struct mca_btl_base_module_t *btl, struct mca_btl_base_endpoint_t *endpoint,
struct mca_btl_base_descriptor_t *descriptor, mca_btl_base_tag_t tag)
{
mca_btl_tcp_module_t *tcp_btl = (mca_btl_tcp_module_t *) btl;
mca_btl_tcp_frag_t *frag = (mca_btl_tcp_frag_t *) descriptor;
int i;

frag->btl = tcp_btl;
frag->endpoint = endpoint;
frag->rc = 0;
frag->iov_idx = 0;
frag->iov_cnt = 1;
frag->iov_ptr = frag->iov;
frag->iov[0].iov_base = (IOVBASE_TYPE *) &frag->hdr;
frag->iov[0].iov_len = sizeof(frag->hdr);
frag->hdr.size = 0;
for (i = 0; i < (int) frag->base.des_segment_count; i++) {
frag->hdr.size += frag->segments[i].seg_len;
frag->iov[i + 1].iov_len = frag->segments[i].seg_len;
frag->iov[i + 1].iov_base = (IOVBASE_TYPE *) frag->segments[i].seg_addr.pval;
frag->iov_cnt++;
}
frag->hdr.base.tag = tag;
frag->hdr.type = MCA_BTL_TCP_HDR_TYPE_SEND;
frag->hdr.count = 0;
if (endpoint->endpoint_nbo) {
MCA_BTL_TCP_HDR_HTON(frag->hdr);
}
return mca_btl_tcp_endpoint_send(endpoint, frag);
}

mca_btl_tcp_endpoint_send尝试发送一个fragment,使用的是endpoint,看起来是一个tcp连接的抽象。如果TCP处于正在连接或者没连接的状态,就发起连接,同时把当前的frag放到list中,因为正在连接,所以没法进行通信。如果已经连接了,调用mca_btl_tcp_frag_send发送frag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
int mca_btl_tcp_endpoint_send(mca_btl_base_endpoint_t *btl_endpoint, mca_btl_tcp_frag_t *frag)
int rc = OPAL_SUCCESS;

OPAL_THREAD_LOCK(&btl_endpoint->endpoint_send_lock);
switch (btl_endpoint->endpoint_state) {
case MCA_BTL_TCP_CONNECTING:
case MCA_BTL_TCP_CONNECT_ACK:
case MCA_BTL_TCP_CLOSED:
opal_list_append(&btl_endpoint->endpoint_frags, (opal_list_item_t *) frag);
frag->base.des_flags |= MCA_BTL_DES_SEND_ALWAYS_CALLBACK;
if (btl_endpoint->endpoint_state == MCA_BTL_TCP_CLOSED) {
rc = mca_btl_tcp_endpoint_start_connect(btl_endpoint);
}
break;
case MCA_BTL_TCP_FAILED:
rc = OPAL_ERR_UNREACH;
break;
case MCA_BTL_TCP_CONNECTED:
if (NULL == btl_endpoint->endpoint_send_frag) {
if (frag->base.des_flags & MCA_BTL_DES_FLAGS_PRIORITY
&& mca_btl_tcp_frag_send(frag, btl_endpoint->endpoint_sd)) {
// 发送成功了应该
int btl_ownership = (frag->base.des_flags & MCA_BTL_DES_FLAGS_BTL_OWNERSHIP);

OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_send_lock);
if (frag->base.des_flags & MCA_BTL_DES_SEND_ALWAYS_CALLBACK) {
frag->base.des_cbfunc(&frag->btl->super, frag->endpoint, &frag->base, frag->rc);
}// 回调函数
if (btl_ownership) {
MCA_BTL_TCP_FRAG_RETURN(frag);
}
MCA_BTL_TCP_ENDPOINT_DUMP(50, btl_endpoint, true,
"complete send fragment [endpoint_send]");
return 1;
} else {
btl_endpoint->endpoint_send_frag = frag;
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, true,
"event_add(send) [endpoint_send]");
frag->base.des_flags |= MCA_BTL_DES_SEND_ALWAYS_CALLBACK;
MCA_BTL_TCP_ACTIVATE_EVENT(&btl_endpoint->endpoint_send_event, 0);
}
} else {
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, true,
"send fragment enqueued [endpoint_send]");
frag->base.des_flags |= MCA_BTL_DES_SEND_ALWAYS_CALLBACK;
opal_list_append(&btl_endpoint->endpoint_frags, (opal_list_item_t *) frag);
}
break;
}
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_send_lock);
return rc;
}

建立TCP连接的函数,首先调用socket建立一个socket,并设置socket的buffer等属性。其次设置这个endpoint的回调函数,一般是设置mca_btl_tcp_endpoint_recv_handlermca_btl_tcp_endpoint_send_handler,应该是在这个socket的某个行为已经完成后的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
static int mca_btl_tcp_endpoint_start_connect(mca_btl_base_endpoint_t *btl_endpoint)
{
int rc, flags;
struct sockaddr_storage endpoint_addr;
/* By default consider a IPv4 connection */
uint16_t af_family = AF_INET;
opal_socklen_t addrlen = sizeof(struct sockaddr_in);

#if OPAL_ENABLE_IPV6
if (AF_INET6 == btl_endpoint->endpoint_addr->addr_family) {
af_family = AF_INET6;
addrlen = sizeof(struct sockaddr_in6);
}
#endif
assert(btl_endpoint->endpoint_sd < 0);
btl_endpoint->endpoint_sd = socket(af_family, SOCK_STREAM, 0);
if (btl_endpoint->endpoint_sd < 0) {
btl_endpoint->endpoint_retries++;
return OPAL_ERR_UNREACH;
}

/* setup socket buffer sizes */
mca_btl_tcp_set_socket_options(btl_endpoint->endpoint_sd);

/* setup event callbacks
只是使用了event_assign,把给定的event类型对象的每一个成员赋予一个指定的值
*/
mca_btl_tcp_endpoint_event_init(btl_endpoint);

/* setup the socket as non-blocking
正如注释所言,只是调用了ioctlsocket设置socket的模式
*/
if ((flags = fcntl(btl_endpoint->endpoint_sd, F_GETFL, 0)) < 0) {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true, opal_process_info.nodename,
getpid(), "fcntl(sd, F_GETFL, 0)", strerror(opal_socket_errno),
opal_socket_errno);
/* Upper layer will handler the error */
return OPAL_ERR_UNREACH;
} else {
flags |= O_NONBLOCK;
if (fcntl(btl_endpoint->endpoint_sd, F_SETFL, flags) < 0) {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true,
opal_process_info.nodename, getpid(),
"fcntl(sd, F_SETFL, flags & O_NONBLOCK)", strerror(opal_socket_errno),
opal_socket_errno);
/* Upper layer will handler the error */
return OPAL_ERR_UNREACH;
}
}

/* 把endpoint_address,可能是地址,转换成sockaddr_storage */
mca_btl_tcp_proc_tosocks(btl_endpoint->endpoint_addr, &endpoint_addr);

/* 将套接字绑定到与此 btl 模块关联的地址之一。 这会将源 IP 设置为在 modex 中共享的地址之一,以便目标 rank 可以正确配对 btl 模块,即使在 Linux 可能对路由做一些意外的情况下
*/
if (endpoint_addr.ss_family == AF_INET) {
assert(NULL != &btl_endpoint->endpoint_btl->tcp_ifaddr);
// 将指定了通信协议的套接字文件与自己的IP和端口绑定起来,sd是socket的编号,tcp_ifaddr是之前转换的ip
// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
if (bind(btl_endpoint->endpoint_sd, (struct sockaddr *) &btl_endpoint->endpoint_btl->tcp_ifaddr, sizeof(struct sockaddr_in)) < 0) {
BTL_ERROR(............);
CLOSE_THE_SOCKET(btl_endpoint->endpoint_sd);
return OPAL_ERROR;
}
}
#if OPAL_ENABLE_IPV6
if (endpoint_addr.ss_family == AF_INET6) {
assert(NULL != &btl_endpoint->endpoint_btl->tcp_ifaddr);
if (bind(btl_endpoint->endpoint_sd, (struct sockaddr *) &btl_endpoint->endpoint_btl->tcp_ifaddr, sizeof(struct sockaddr_in6)) < 0) {
BTL_ERROR(............);
CLOSE_THE_SOCKET(btl_endpoint->endpoint_sd);
return OPAL_ERROR;
}
}
#endif

if (0 == connect(btl_endpoint->endpoint_sd, (struct sockaddr *) &endpoint_addr, addrlen)) {
// 连接socket
// int connect (int sockfd, struct sockaddr * serv_addr, int addrlen)
/* send our globally unique process identifier to the endpoint */
if ((rc = mca_btl_tcp_endpoint_send_connect_ack(btl_endpoint)) == OPAL_SUCCESS) {
// 最终是调用了send函数进行发送magic id的操作
btl_endpoint->endpoint_state = MCA_BTL_TCP_CONNECT_ACK;
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, true, "event_add(recv) [start_connect]");
opal_event_add(&btl_endpoint->endpoint_recv_event, 0);
if (mca_btl_tcp_event_base == opal_sync_event_base) {
/* If no progress thread then raise the awarness of the default progress engine */
opal_progress_event_users_increment();
}
return OPAL_SUCCESS;
}
/* We connected to the peer, but he close the socket before we got a chance to send our guid
*/
MCA_BTL_TCP_ENDPOINT_DUMP(1, btl_endpoint, true, "dropped connection [start_connect]");
} else {
/* non-blocking so wait for completion */
if (opal_socket_errno == EINPROGRESS || opal_socket_errno == EWOULDBLOCK) {
btl_endpoint->endpoint_state = MCA_BTL_TCP_CONNECTING;
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, true, "event_add(send) [start_connect]");
MCA_BTL_TCP_ACTIVATE_EVENT(&btl_endpoint->endpoint_send_event, 0);
opal_output_verbose(30, opal_btl_base_framework.framework_output,
"btl:tcp: would block, so allowing background progress");
return OPAL_SUCCESS;
}
}

{
char *address;
address = opal_net_get_hostname((struct sockaddr *) &endpoint_addr);
BTL_PEER_ERROR(btl_endpoint->endpoint_proc->proc_opal,
("Unable to connect to the peer %s on port %d: %s\n", address,
ntohs(btl_endpoint->endpoint_addr->addr_port),
strerror(opal_socket_errno)));
}
btl_endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(btl_endpoint);
return OPAL_ERR_UNREACH;
}

两个回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/*
* A file descriptor is available/ready for recv. Check the state
* of the socket and take the appropriate action.
*/

static void mca_btl_tcp_endpoint_recv_handler(int sd, short flags, void *user)
{
mca_btl_base_endpoint_t *btl_endpoint = (mca_btl_base_endpoint_t *) user;

/* Make sure we don't have a race between a thread that remove the
* recv event, and one event already scheduled.
*/
if (sd != btl_endpoint->endpoint_sd) {
return;
}

/**
* 这里有一个极其罕见的竞争条件,只能在初始化期间触发。
* 如果两个进程同时启动它们的连接,则其中一个进程将不得不关闭它的前一个endpoint(从本地发送打开的那个)。
* 结果它可能会进入 btl_endpoint_close 并尝试删除 recv_event。
* 此调用将返回 libevent,并且在多线程情况下将尝试锁定事件。
* 如果另一个线程注意到活动事件(这是可能的,因为在初始化期间将有 2 个套接字),
* 一个线程可能会卡住试图锁定 endpoint_recv_lock(同时持有 event_base 锁),
* 而另一个线程将尝试锁定 event_base 锁(同时持有 endpoint_recv 锁)。

如果我们不能锁定这个互斥体,取消接收操作是可以的,它最终会很快再次触发。
*/
if (OPAL_THREAD_TRYLOCK(&btl_endpoint->endpoint_recv_lock)) {
return;
}

switch (btl_endpoint->endpoint_state) {
case MCA_BTL_TCP_CONNECT_ACK: {
int rc = mca_btl_tcp_endpoint_recv_connect_ack(btl_endpoint);
// 如果还是MCA_BTL_TCP_CONNECT_ACK,说明可能还不用真的接收真实数据
// 最终调用的是recv函数,接收标识符确认已经完成了连接
// 把这个endpoint设置为MCA_BTL_TCP_CONNECTED
if (OPAL_SUCCESS == rc) {
/* we are now connected. Start sending the data */
OPAL_THREAD_LOCK(&btl_endpoint->endpoint_send_lock);
mca_btl_tcp_endpoint_connected(btl_endpoint);
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_send_lock);
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, true, "connected");
} else if (OPAL_ERR_BAD_PARAM == rc || OPAL_ERROR == rc) {
/* If we get a BAD_PARAM, it means that it probably wasn't
an OMPI process on the other end of the socket (e.g.,
the magic string ID failed). recv_connect_ack already cleaned
up the socket. */
/* If we get OPAL_ERROR, the other end closed the connection
* because it has initiated a symetrical connexion on its end.
* recv_connect_ack already cleaned up the socket. */
} else {
/* Otherwise, it probably *was* an OMPI peer process on
the other end, and something bad has probably
happened. */
mca_btl_tcp_module_t *m = btl_endpoint->endpoint_btl;

/* Fail up to the PML */
if (NULL != m->tcp_error_cb) {
m->tcp_error_cb(
(mca_btl_base_module_t *) m, MCA_BTL_ERROR_FLAGS_FATAL,
btl_endpoint->endpoint_proc->proc_opal,
"TCP ACK is neither SUCCESS nor ERR (something bad has probably happened)");
}
}
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_recv_lock);
return;
}
case MCA_BTL_TCP_CONNECTED: {
// 如果已经是MCA_BTL_TCP_CONNECTED状态了,执行接收
mca_btl_tcp_frag_t *frag;

frag = btl_endpoint->endpoint_recv_frag;
if (NULL == frag) {
if (mca_btl_tcp_module.super.btl_max_send_size
> mca_btl_tcp_module.super.btl_eager_limit) {
MCA_BTL_TCP_FRAG_ALLOC_MAX(frag);
} else {
MCA_BTL_TCP_FRAG_ALLOC_EAGER(frag);
}
// 从opal_free_list_item_t表里找到一个需要接收的,之前好像有把消息加入到这里
if (NULL == frag) {
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_recv_lock);
return;
}
MCA_BTL_TCP_FRAG_INIT_DST(frag, btl_endpoint);
}

#if MCA_BTL_TCP_ENDPOINT_CACHE
assert(0 == btl_endpoint->endpoint_cache_length);
data_still_pending_on_endpoint:
#endif /* MCA_BTL_TCP_ENDPOINT_CACHE */
/* check for completion of non-blocking recv on the current fragment */
if (mca_btl_tcp_frag_recv(frag, btl_endpoint->endpoint_sd) == false) {
// 这个函数关键是readv,与之前的sendv对应,其他很多代码是处理接收了一部分frag或者接收失败的情况。
btl_endpoint->endpoint_recv_frag = frag;
} else {
btl_endpoint->endpoint_recv_frag = NULL;
if (MCA_BTL_TCP_HDR_TYPE_SEND == frag->hdr.type) {
mca_btl_active_message_callback_t *reg = mca_btl_base_active_message_trigger
+ frag->hdr.base.tag;
const mca_btl_base_receive_descriptor_t desc
= {.endpoint = btl_endpoint,
.des_segments = frag->base.des_segments,
.des_segment_count = frag->base.des_segment_count,
.tag = frag->hdr.base.tag,
.cbdata = reg->cbdata};
reg->cbfunc(&frag->btl->super, &desc);
}
#if MCA_BTL_TCP_ENDPOINT_CACHE
if (0 != btl_endpoint->endpoint_cache_length) {
/* 如果还有数据在frag里的话,重用它
*/
MCA_BTL_TCP_FRAG_INIT_DST(frag, btl_endpoint);
goto data_still_pending_on_endpoint;
}
#endif /* MCA_BTL_TCP_ENDPOINT_CACHE */
MCA_BTL_TCP_FRAG_RETURN(frag);
}
#if MCA_BTL_TCP_ENDPOINT_CACHE
assert(0 == btl_endpoint->endpoint_cache_length);
#endif /* MCA_BTL_TCP_ENDPOINT_CACHE */
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_recv_lock);
break;
}
case MCA_BTL_TCP_CLOSED:
/* 这是一个线程安全问题。
* 由于允许多个线程生成事件,
* 当我们到达 MPI_Finalize 的末尾时,
* 我们最终会有多个线程执行接收回调。
* 第一个将关闭连接,所有其他人都会抱怨。
*/
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_recv_lock);
break;
default:
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_recv_lock);
BTL_ERROR(("invalid socket state(%d)", btl_endpoint->endpoint_state));
btl_endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(btl_endpoint);
break;
}
}

/*
* A file descriptor is available/ready for send. Check the state
* of the socket and take the appropriate action.
*/

static void mca_btl_tcp_endpoint_send_handler(int sd, short flags, void *user)
{
mca_btl_tcp_endpoint_t *btl_endpoint = (mca_btl_tcp_endpoint_t *) user;

/* if another thread is already here, give up */
if (OPAL_THREAD_TRYLOCK(&btl_endpoint->endpoint_send_lock)) {
return;
}

switch (btl_endpoint->endpoint_state) {
case MCA_BTL_TCP_CONNECTING:
mca_btl_tcp_endpoint_complete_connect(btl_endpoint);
// 检查是否已经连接了socket,如果连上了,就发送进程标识符
break;
case MCA_BTL_TCP_CONNECTED:
/* complete the current send */
while (NULL != btl_endpoint->endpoint_send_frag) {
// 如果一直有frag需要发送
mca_btl_tcp_frag_t *frag = btl_endpoint->endpoint_send_frag;
int btl_ownership = (frag->base.des_flags & MCA_BTL_DES_FLAGS_BTL_OWNERSHIP);

assert(btl_endpoint->endpoint_state == MCA_BTL_TCP_CONNECTED);
if (mca_btl_tcp_frag_send(frag, btl_endpoint->endpoint_sd) == false) {
// 发送,如果失败的话直接跳出去
break;
}
/* progress any pending sends 找到其他需要发送的frag 尝试发送*/
btl_endpoint->endpoint_send_frag = (mca_btl_tcp_frag_t *) opal_list_remove_first(
&btl_endpoint->endpoint_frags);

/* if required - update request status and release fragment */
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_send_lock);
assert(frag->base.des_flags & MCA_BTL_DES_SEND_ALWAYS_CALLBACK);
if (NULL != frag->base.des_cbfunc) {
frag->base.des_cbfunc(&frag->btl->super, frag->endpoint, &frag->base, frag->rc);
}
if (btl_ownership) {
MCA_BTL_TCP_FRAG_RETURN(frag);
}
/* if we fail to take the lock simply return. In the worst case the
* send_handler will be triggered once more, and as there will be
* nothing to send the handler will be deleted.
*/
if (OPAL_THREAD_TRYLOCK(&btl_endpoint->endpoint_send_lock)) {
return;
}
}

/* if nothing else to do unregister for send event notifications */
if (NULL == btl_endpoint->endpoint_send_frag) {
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, false,
"event_del(send) [endpoint_send_handler]");
opal_event_del(&btl_endpoint->endpoint_send_event);
}
break;
case MCA_BTL_TCP_FAILED:
MCA_BTL_TCP_ENDPOINT_DUMP(1, btl_endpoint, true,
"event_del(send) [endpoint_send_handler:error]");
opal_event_del(&btl_endpoint->endpoint_send_event);
break;
default:
BTL_ERROR(("invalid connection state (%d)", btl_endpoint->endpoint_state));
MCA_BTL_TCP_ENDPOINT_DUMP(1, btl_endpoint, true,
"event_del(send) [endpoint_send_handler:error]");
opal_event_del(&btl_endpoint->endpoint_send_event);
break;
}
OPAL_THREAD_UNLOCK(&btl_endpoint->endpoint_send_lock);
}

这是检查socket是否连接的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* 检查连接状态。 如果连接失败,稍后将重试。 否则,将此进程标识符发送到新连接的套接字上的端点。
*/
static int mca_btl_tcp_endpoint_complete_connect(mca_btl_base_endpoint_t *btl_endpoint)
{
int so_error = 0;
opal_socklen_t so_length = sizeof(so_error);
struct sockaddr_storage endpoint_addr;

/* Delete the send event notification, as the next step is waiting for the ack
* from the peer. Once this ack is received we will deal with the send notification
* accordingly.
*/
opal_event_del(&btl_endpoint->endpoint_send_event);

mca_btl_tcp_proc_tosocks(btl_endpoint->endpoint_addr, &endpoint_addr);
// 把内部的proc_addr->addr_union.addr_inet转成socket用的类型addr

/* check connect completion status */
if (getsockopt(btl_endpoint->endpoint_sd, SOL_SOCKET, SO_ERROR, (char *) &so_error, &so_length) < 0) {
// 获取socket的属性
btl_endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(btl_endpoint);
return OPAL_ERROR;
}
if (so_error == EINPROGRESS || so_error == EWOULDBLOCK) {
return OPAL_SUCCESS;
}
if (so_error != 0) {
if (mca_btl_base_warn_peer_error || mca_btl_base_verbose > 0) {
char *msg;
free(msg);
}
btl_endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(btl_endpoint);
return OPAL_ERROR;
}

if (mca_btl_tcp_endpoint_send_connect_ack(btl_endpoint) == OPAL_SUCCESS) {
// 最终调用了send函数发送出去
btl_endpoint->endpoint_state = MCA_BTL_TCP_CONNECT_ACK;
opal_event_add(&btl_endpoint->endpoint_recv_event, 0);
if (mca_btl_tcp_event_base == opal_sync_event_base) {
/* If no progress thread then raise the awarness of the default progress engine */
opal_progress_event_users_increment();
}
MCA_BTL_TCP_ENDPOINT_DUMP(10, btl_endpoint, false, "event_add(recv) [complete_connect]");
return OPAL_SUCCESS;
}
MCA_BTL_TCP_ENDPOINT_DUMP(1, btl_endpoint, false, " [complete_connect]");
btl_endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(btl_endpoint);
return OPAL_ERROR;
}

经过了这么长一块总算建立好了连接,接下来是执行发送的函数,writev是将不连续的内存块写入地址中,这里的参数sd是socket的编号。可能出现没写完的情况,这时更新frag的iov_cnt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bool mca_btl_tcp_frag_send(mca_btl_tcp_frag_t *frag, int sd)
{
ssize_t cnt;
size_t i, num_vecs;

/* non-blocking write, but continue if interrupted */
do {
cnt = writev(sd, frag->iov_ptr, frag->iov_cnt);
if (cnt < 0) {
switch (opal_socket_errno) {
case EINTR:
continue;
case EWOULDBLOCK:
return false;
case EFAULT:
BTL_ERROR(("mca_btl_tcp_frag_send: writev error (%p, %lu)\n\t%s(%lu)\n",
frag->iov_ptr[0].iov_base, (unsigned long) frag->iov_ptr[0].iov_len,
strerror(opal_socket_errno), (unsigned long) frag->iov_cnt));
/* send_lock held by caller */
frag->endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(frag->endpoint);
return false;
default:
BTL_PEER_ERROR(frag->endpoint->endpoint_proc->proc_opal,
("mca_btl_tcp_frag_send: writev failed: %s (%d)",
strerror(opal_socket_errno), opal_socket_errno));
/* send_lock held by caller */
frag->endpoint->endpoint_state = MCA_BTL_TCP_FAILED;
mca_btl_tcp_endpoint_close(frag->endpoint);
return false;
}
}
} while (cnt < 0);

/* if the write didn't complete - update the iovec state */
num_vecs = frag->iov_cnt;
for (i = 0; i < num_vecs; i++) {
if (cnt >= (ssize_t) frag->iov_ptr->iov_len) {
cnt -= frag->iov_ptr->iov_len;
frag->iov_ptr++;
frag->iov_idx++;
frag->iov_cnt--;
} else {
frag->iov_ptr->iov_base = (opal_iov_base_ptr_t)(
((unsigned char *) frag->iov_ptr->iov_base) + cnt);
frag->iov_ptr->iov_len -= cnt;
OPAL_OUTPUT_VERBOSE((100, opal_btl_base_framework.framework_output,
"%s:%d write %ld bytes on socket %d\n", __FILE__, __LINE__, cnt,
sd));
break;
}
}
return (frag->iov_cnt == 0);
}

mca_btl_tcp_create在给定设备(即 kindex)上查找未被 disable_family 选项禁用的地址。 如果没有,请跳过为此接口创建模块。 我们将地址存储在模块上,既可以在 modex 中发布,也可以用作该模块发送的所有数据包的源地址。 最好将 split_and_resolve 分开并将用于选择设备的地址传递给mca_btl_tcp_create()。 这是对多年来一直使用的逻辑的清理,但它没有涵盖的情况是(例如)仅在接口具有 10.0.0.1 和 10.1.0.1 的地址时指定 mca_btl_if_include 10.0.0.0/16; 绝对没有什么可以阻止此代码选择 10.1.0.1 作为在 modex 中发布并用于连接的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

/*
* Create a btl instance and add to modules list.
*/

static int mca_btl_tcp_create(const int if_kindex, const char *if_name)
{
struct mca_btl_tcp_module_t *btl;
opal_if_t *copied_interface, *selected_interface;
char param[256];
int i, if_index;
struct sockaddr_storage addr;
bool found = false;

OPAL_LIST_FOREACH (selected_interface, &opal_if_list, opal_if_t) {
if (if_kindex != selected_interface->if_kernel_index) {
continue;
}

if_index = selected_interface->if_index;

memcpy((struct sockaddr *) &addr, &selected_interface->if_addr,
MIN(sizeof(struct sockaddr_storage), sizeof(selected_interface->if_addr)));

if (addr.ss_family == AF_INET && 4 != mca_btl_tcp_component.tcp_disable_family) {
found = true;
break;
} else if (addr.ss_family == AF_INET6 && 6 != mca_btl_tcp_component.tcp_disable_family) {
found = true;
break;
}
}
/* 如果没找到就返回 */
if (!found) {
return OPAL_SUCCESS;
}

for (i = 0; i < (int) mca_btl_tcp_component.tcp_num_links; i++) {
btl = (struct mca_btl_tcp_module_t *) malloc(sizeof(mca_btl_tcp_module_t));
if (NULL == btl) {
return OPAL_ERR_OUT_OF_RESOURCE;
}
copied_interface = OBJ_NEW(opal_if_t);
if (NULL == copied_interface) {
free(btl);
return OPAL_ERR_OUT_OF_RESOURCE;
}
memcpy(btl, &mca_btl_tcp_module, sizeof(mca_btl_tcp_module));
OBJ_CONSTRUCT(&btl->tcp_endpoints, opal_list_t);
OBJ_CONSTRUCT(&btl->tcp_endpoints_mutex, opal_mutex_t);
mca_btl_tcp_component.tcp_btls[mca_btl_tcp_component.tcp_num_btls++] = btl;

/* initialize the btl */
/* This index is used as a key for a hash table used for interface matching. */
btl->btl_index = mca_btl_tcp_component.tcp_num_btls - 1;
btl->tcp_ifkindex = (uint16_t) if_kindex;
#if MCA_BTL_TCP_STATISTICS
btl->tcp_bytes_recv = 0;
btl->tcp_bytes_sent = 0;
btl->tcp_send_handler = 0;
#endif

memcpy(&btl->tcp_ifaddr, &addr, sizeof(struct sockaddr_storage));
btl->tcp_ifmask = selected_interface->if_mask;

/* allow user to specify interface bandwidth */
sprintf(param, "bandwidth_%s", if_name);
mca_btl_tcp_param_register_uint(param, NULL, btl->super.btl_bandwidth, OPAL_INFO_LVL_5,
&btl->super.btl_bandwidth);

/* allow user to override/specify latency ranking */
sprintf(param, "latency_%s", if_name);
mca_btl_tcp_param_register_uint(param, NULL, btl->super.btl_latency, OPAL_INFO_LVL_5,
&btl->super.btl_latency);
if (i > 0) {
btl->super.btl_bandwidth >>= 1;
btl->super.btl_latency <<= 1;
}


/* 注册一些参数 */
sprintf(param, "bandwidth_%s:%d", if_name, i);
mca_btl_tcp_param_register_uint(param, NULL, btl->super.btl_bandwidth, OPAL_INFO_LVL_5,
&btl->super.btl_bandwidth);

/* allow user to override/specify latency ranking */
sprintf(param, "latency_%s:%d", if_name, i);
mca_btl_tcp_param_register_uint(param, NULL, btl->super.btl_latency, OPAL_INFO_LVL_5,
&btl->super.btl_latency);

/* Only attempt to auto-detect bandwidth and/or latency if it is 0.
*
* If detection fails to return anything other than 0, set a default
* bandwidth and latency.
*/
if (0 == btl->super.btl_bandwidth) {
// 如果能用ethtool 的话使用这个工具自动监测带宽
unsigned int speed = opal_ethtool_get_speed(if_name);
btl->super.btl_bandwidth = (speed == 0) ? MCA_BTL_TCP_BTL_BANDWIDTH : speed;
if (i > 0) {
btl->super.btl_bandwidth >>= 1;
}
}
/* We have no runtime btl latency detection mechanism. Just set a default. */
if (0 == btl->super.btl_latency) {
btl->super.btl_latency = MCA_BTL_TCP_BTL_LATENCY;
if (i > 0) {
btl->super.btl_latency <<= 1;
}
}

/* Add another entry to the local interface list */
opal_string_copy(copied_interface->if_name, if_name, OPAL_IF_NAMESIZE);
copied_interface->if_index = if_index;
copied_interface->if_kernel_index = btl->tcp_ifkindex;
copied_interface->af_family = btl->tcp_ifaddr.ss_family;
copied_interface->if_flags = selected_interface->if_flags;
copied_interface->if_speed = selected_interface->if_speed;
memcpy(&copied_interface->if_addr, &btl->tcp_ifaddr, sizeof(struct sockaddr_storage));
copied_interface->if_mask = selected_interface->if_mask;
copied_interface->if_bandwidth = btl->super.btl_bandwidth;
memcpy(&copied_interface->if_mac, &selected_interface->if_mac,
sizeof(copied_interface->if_mac));
copied_interface->ifmtu = selected_interface->ifmtu;

opal_list_append(&mca_btl_tcp_component.local_ifs, &(copied_interface->super));
}
return OPAL_SUCCESS;
}

当引擎发现socket有连接事件是,调用这个函数进行accept,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

/**
int accept ( int s , struct sockaddr *addr , socklen_t *addrlen ) ;

参数
1. s : 是服务器端通过调用正确调用socket -> bind -> listen 函数之后的用于指向存放多个客户端缓冲
队列缓冲区的套接字描述符
2. addr : 是用来保存发起连接请求的主机的地址与端口的结构体变量,就是存放服务器接收请求
的客户端的网络地址与端口的结构体变量
3. addrlen: 用来传入第二个参数类型长度

返回值:
如果函数执行正确的话,将会返回新的套接字描述符,用于指向与当前通信的客户端交换数据的缓冲区的套接字描述符
*/
static void mca_btl_tcp_component_accept_handler(int incoming_sd, short ignored, void *unused)
{
while (true) {
#if OPAL_ENABLE_IPV6
struct sockaddr_in6 addr;
#else
struct sockaddr_in addr;
#endif
opal_socklen_t addrlen = sizeof(addr);

mca_btl_tcp_event_t *event;
int sd = accept(incoming_sd, (struct sockaddr *) &addr, &addrlen);
if (sd < 0) {
if (opal_socket_errno == EINTR) {
continue;
}
if (opal_socket_errno != EAGAIN && opal_socket_errno != EWOULDBLOCK) {
opal_show_help("help-mpi-btl-tcp.txt", "accept failed", true,
opal_process_info.nodename, getpid(), opal_socket_errno,
strerror(opal_socket_errno));
}
return;
}
mca_btl_tcp_set_socket_options(sd);

assert(NULL != mca_btl_tcp_event_base);
/* wait for receipt of peers process identifier to complete this connection */
event = OBJ_NEW(mca_btl_tcp_event_t);
opal_event_set(mca_btl_tcp_event_base, &(event->event), sd, OPAL_EV_READ,
mca_btl_tcp_component_recv_handler, event);
opal_event_add(&event->event, 0);
}
}

/**
* Event callback when there is data available on the registered
* socket to recv. This callback is triggered only once per lifetime
* for any socket, in the beginning when we setup the handshake
* protocol.
*/
static void mca_btl_tcp_component_recv_handler(int sd, short flags, void *user)
{
mca_btl_tcp_event_t *event = (mca_btl_tcp_event_t *) user;
opal_process_name_t guid;
struct sockaddr_storage addr;
opal_socklen_t addr_len = sizeof(addr);
mca_btl_tcp_proc_t *btl_proc;
bool sockopt = true;
size_t retval, len = strlen(mca_btl_tcp_magic_id_string);
mca_btl_tcp_endpoint_hs_msg_t hs_msg;
struct timeval save, tv;
socklen_t rcvtimeo_save_len = sizeof(save);

/* Note, Socket will be in blocking mode during intial handshake
* hence setting SO_RCVTIMEO to say 2 seconds here to avoid waiting
* forever when connecting to older versions (that reply to the
* handshake with only the guid) or when the remote side isn't OMPI
*/

/* get the current timeout value so we can reset to it */
if (0 != getsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, (void *) &save, &rcvtimeo_save_len)) {
if (ENOPROTOOPT == errno || EOPNOTSUPP == errno) {
sockopt = false;
} else {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true,
opal_process_info.nodename, getpid(),
"getsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, ...)",
strerror(opal_socket_errno), opal_socket_errno);
return;
}
} else {
tv.tv_sec = 2;
tv.tv_usec = 0;
if (0 != setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true,
opal_process_info.nodename, getpid(),
"setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, ...)",
strerror(opal_socket_errno), opal_socket_errno);
return;
}
}

OBJ_RELEASE(event);
retval = mca_btl_tcp_recv_blocking(sd, (void *) &hs_msg, sizeof(hs_msg));

/*
* 如果我们收到一条长度为零的消息,很可能我们同时连接到 Open MPI 对等进程 X,而对等方关闭了与我们的连接(有利于我们与它们的连接)。
* 这不是错误 - 只需将其关闭并继续。
* 同样,如果我们得到的字节数少于 sizeof(hs_msg),它可能不是 Open MPI 对等体。
* 但我们并不在意,因为对等方关闭了套接字。 所以只需关闭它并继续前进。
*/
if (retval < sizeof(hs_msg)) {
const char *peer = opal_fd_get_peer_name(sd);
opal_output_verbose(
20, opal_btl_base_framework.framework_output,
"Peer %s closed socket without sending BTL TCP magic ID handshake (we received %d "
"bytes out of the expected %d) -- closing/ignoring this connection",
peer, (int) retval, (int) sizeof(hs_msg));
free((char *) peer);
CLOSE_THE_SOCKET(sd);
return;
}

/* 确认这个字符串是不是magic,来确认是不是openmpi的进程 */
guid = hs_msg.guid;
if (0 != strncmp(hs_msg.magic_id, mca_btl_tcp_magic_id_string, len)) {
const char *peer = opal_fd_get_peer_name(sd);
opal_output_verbose(
20, opal_btl_base_framework.framework_output,
"Peer %s send us an incorrect Open MPI magic ID string (i.e., this was not a "
"connection from the same version of Open MPI; expected \"%s\", received \"%s\")",
peer, mca_btl_tcp_magic_id_string, hs_msg.magic_id);
free((char *) peer);

/* The other side probably isn't OMPI, so just hang up */
CLOSE_THE_SOCKET(sd);
return;
}

if (sockopt) {
/* reset RECVTIMEO option to its original state */
if (0 != setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &save, sizeof(save))) {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true,
opal_process_info.nodename, getpid(),
"setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, ...)",
strerror(opal_socket_errno), opal_socket_errno);
return;
}
}

OPAL_PROCESS_NAME_NTOH(guid);

/* now set socket up to be non-blocking */
if ((flags = fcntl(sd, F_GETFL, 0)) < 0) {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true, opal_process_info.nodename,
getpid(), "fcntl(sd, F_GETFL, 0)", strerror(opal_socket_errno),
opal_socket_errno);
CLOSE_THE_SOCKET(sd);
} else {
flags |= O_NONBLOCK;
if (fcntl(sd, F_SETFL, flags) < 0) {
opal_show_help("help-mpi-btl-tcp.txt", "socket flag fail", true,
opal_process_info.nodename, getpid(),
"fcntl(sd, F_SETFL, flags & O_NONBLOCK)", strerror(opal_socket_errno),
opal_socket_errno);
CLOSE_THE_SOCKET(sd);
}
}

/* lookup the corresponding process */
btl_proc = mca_btl_tcp_proc_lookup(&guid);
if (NULL == btl_proc) {
opal_show_help("help-mpi-btl-tcp.txt", "server accept cannot find guid", true,
opal_process_info.nodename, getpid());
CLOSE_THE_SOCKET(sd);
return;
}

/* lookup peer address */
if (getpeername(sd, (struct sockaddr *) &addr, &addr_len) != 0) {
if (ENOTCONN != opal_socket_errno) {
opal_show_help("help-mpi-btl-tcp.txt", "server getpeername failed", true,
opal_process_info.nodename, getpid(), strerror(opal_socket_errno),
opal_socket_errno);
}
CLOSE_THE_SOCKET(sd);
return;
}

/* are there any existing peer instances willing to accept this connection */
(void) mca_btl_tcp_proc_accept(btl_proc, (struct sockaddr *) &addr, sd);

const char *str = opal_fd_get_peer_name(sd);
opal_output_verbose(10, opal_btl_base_framework.framework_output,
"btl:tcp: now connected to %s, process %s", str,
OPAL_NAME_PRINT(btl_proc->proc_opal->proc_name));
free((char *) str);
}

RDMA

以下缕一下RDMA的执行过程。支持RDMA的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief osc rdma component structure
*/
struct ompi_osc_rdma_component_t {
/** Extend the basic osc component interface */
ompi_osc_base_component_t super;
/** lock access to modules */
opal_mutex_t lock;
/** cid -> module mapping */
opal_hash_table_t modules;
/** free list of ompi_osc_rdma_frag_t structures */
opal_free_list_t frags;
/** Free list of requests */
opal_free_list_t requests;
/** RDMA component buffer size */
unsigned int buffer_size;
/** List of requests that need to be freed */
opal_list_t request_gc;
/** List of buffers that need to be freed */
opal_list_t buffer_gc;
/** Maximum number of segments that can be attached to a dynamic window */
unsigned int max_attach;
/** Default value of the no_locks info key for new windows */
bool no_locks;
/** Locking mode to use as the default for all windows */
int locking_mode;
/** Accumulate operations will only operate on a single intrinsic datatype */
bool acc_single_intrinsic;
/** Use network AMOs when available */
bool acc_use_amo;
/** Priority of the osc/rdma component */
unsigned int priority;
/** directory where to place backing files */
char *backing_directory;
/** maximum count for network AMO usage */
unsigned long network_amo_max_count;
/** memory alignmen to be used for new windows */
size_t memory_alignment;
};
typedef struct ompi_osc_rdma_component_t ompi_osc_rdma_component_t;

每个 MPI 窗口都与单个 osc 模块相关联。 该结构存储与 osc/rdma 组件相关的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
struct ompi_osc_rdma_module_t {
/** Extend the basic osc module interface */
ompi_osc_base_module_t super;
/** pointer back to MPI window */
struct ompi_win_t *win;
/** Mutex lock protecting module data */
opal_mutex_t lock;
/** locking mode to use */
int locking_mode;
/* window configuration */
/** value of same_disp_unit info key for this window */
bool same_disp_unit;
/** value of same_size info key for this window */
bool same_size;
/** passive-target synchronization will not be used in this window */
bool no_locks;
bool acc_single_intrinsic;
bool acc_use_amo;
/** whether the group is located on a single node */
bool single_node;
/** flavor of this window */
int flavor;
/** size of local window */
size_t size;
/** Local displacement unit. */
int disp_unit;
/** maximum count for network AMO usage */
unsigned long network_amo_max_count;
/** global leader */
ompi_osc_rdma_peer_t *leader;
/** my peer structure */
ompi_osc_rdma_peer_t *my_peer;
/** pointer to free on cleanup (may be NULL) */
void *free_after;
/** local state structure (shared memory) */
ompi_osc_rdma_state_t *state;
/** node-level communication data (shared memory) */
unsigned char *node_comm_info;
/* only relevant on the lowest rank on each node (shared memory) */
ompi_osc_rdma_rank_data_t *rank_array;
/** communicator created with this window. This is the cid used
* in the component's modules mapping. */
ompi_communicator_t *comm;
/* temporary communicators for window initialization */
ompi_communicator_t *local_leaders;
ompi_communicator_t *shared_comm;
/** node id of this rank */
int node_id;
/** number of nodes */
int node_count;
/** handle valid for local state (valid for local data for MPI_Win_allocate) */
mca_btl_base_registration_handle_t *state_handle;
/** registration handle for the window base (only used for MPI_Win_create) */
mca_btl_base_registration_handle_t *base_handle;
/** size of a region */
size_t region_size;
/** size of the state structure */
size_t state_size;
/** offset in the shared memory segment where the state array starts */
size_t state_offset;
/** memory alignmen to be used for new windows */
size_t memory_alignment;

/* ********************* sync data ************************ */
/** global sync object (PSCW, fence, lock all) */
ompi_osc_rdma_sync_t all_sync;
/** current group associate with pscw exposure epoch */
struct ompi_group_t *pw_group;
/** list of unmatched post messages */
opal_list_t pending_posts;

/* ********************* LOCK data ************************ */
/** number of outstanding locks */
osc_rdma_counter_t passive_target_access_epoch;
/** origin side list of locks currently outstanding */
opal_hash_table_t outstanding_locks;
/** array of locks (small jobs) */
ompi_osc_rdma_sync_t **outstanding_lock_array;

/* ******************* peer storage *********************** */
/** hash table of allocated peers */
opal_hash_table_t peer_hash;
/** array of allocated peers (small jobs) */
ompi_osc_rdma_peer_t **peer_array;
/** lock for peer hash table/array */
opal_mutex_t peer_lock;

/* ******************* communication *********************** */

/*
* 我们目前支持两种操作模式,一个加速 btl(可以使用内存注册,可以使用 btl_flush() 和一个或多个备用 btl,
* 它不能使用 flush() 或依赖内存注册。因为它是一个非此即彼的 情况下,我们使用联合来简化代码。
*/
bool use_accelerated_btl;

union {
struct {
mca_btl_base_module_t *accelerated_btl;
};
struct {
mca_btl_base_am_rdma_module_t **alternate_am_rdmas;
uint8_t alternate_btl_count;
};
};

/*
* 选择的 BTL 是否需要内存注册? 使用备用 BTL 时该字段为 false,使用加速 BTL 时的值取决于底层 BTL 的注册要求。
*/
bool use_memory_registration;

size_t put_alignment;
size_t get_alignment;
size_t put_limit;
size_t get_limit;

uint32_t atomic_flags;

/** registered fragment used for locally buffered RDMA transfers */
struct ompi_osc_rdma_frag_t *rdma_frag;

/** registration handles for dynamically attached regions. These are not stored
* in the state structure as it is entirely local. */
ompi_osc_rdma_handle_t **dynamic_handles;

/* 共享内存段。
* 此段包含此节点的排名部分 -> 节点映射数组、节点通信数据 (node_comm_info)、
* 所有本地排名的状态和所有本地排名的数据(仅限 MPI_Win_allocate)
*/
void *segment_base;

/** opal shared memory structure for the shared memory segment */
opal_shmem_ds_t seg_ds;
/* performance values */
/** number of times a put had to be retried */
unsigned long put_retry_count;
/** number of time a get had to be retried */
unsigned long get_retry_count;
/** outstanding atomic operations */
opal_atomic_int32_t pending_ops;
};

这是rdma相关的一些函数,主要看ompi_osc_rdma_getompi_osc_rdma_put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ompi_osc_base_module_t ompi_osc_rdma_module_rdma_template = {
.osc_win_attach = ompi_osc_rdma_attach,
.osc_win_detach = ompi_osc_rdma_detach,
.osc_free = ompi_osc_rdma_free,

.osc_put = ompi_osc_rdma_put,
.osc_get = ompi_osc_rdma_get,
.osc_accumulate = ompi_osc_rdma_accumulate,
.osc_compare_and_swap = ompi_osc_rdma_compare_and_swap,
.osc_fetch_and_op = ompi_osc_rdma_fetch_and_op,
.osc_get_accumulate = ompi_osc_rdma_get_accumulate,

.osc_rput = ompi_osc_rdma_rput,
.osc_rget = ompi_osc_rdma_rget,
.osc_raccumulate = ompi_osc_rdma_raccumulate,
.osc_rget_accumulate = ompi_osc_rdma_rget_accumulate,

.osc_fence = ompi_osc_rdma_fence_atomic,

.osc_start = ompi_osc_rdma_start_atomic,
.osc_complete = ompi_osc_rdma_complete_atomic,
.osc_post = ompi_osc_rdma_post_atomic,
.osc_wait = ompi_osc_rdma_wait_atomic,
.osc_test = ompi_osc_rdma_test_atomic,

.osc_lock = ompi_osc_rdma_lock_atomic,
.osc_unlock = ompi_osc_rdma_unlock_atomic,
.osc_lock_all = ompi_osc_rdma_lock_all_atomic,
.osc_unlock_all = ompi_osc_rdma_unlock_all_atomic,

.osc_sync = ompi_osc_rdma_sync,
.osc_flush = ompi_osc_rdma_flush,
.osc_flush_all = ompi_osc_rdma_flush_all,
.osc_flush_local = ompi_osc_rdma_flush_local,
.osc_flush_local_all = ompi_osc_rdma_flush_local_all,
};

ompi_osc_rdma_get输出一些log后,查找跟当前的source_rank相关的结构,之后的数据从source_rank里拿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
int ompi_osc_rdma_get (void *origin_addr, int origin_count, ompi_datatype_t *origin_datatype,
int source_rank, ptrdiff_t source_disp, int source_count,
ompi_datatype_t *source_datatype, ompi_win_t *win)
{
ompi_osc_rdma_module_t *module = GET_MODULE(win);
ompi_osc_rdma_peer_t *peer;
ompi_osc_rdma_sync_t *sync;

OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_TRACE, "get: 0x%lx, %d, %s, %d, %d, %d, %s, %s", (unsigned long) origin_addr,
origin_count, origin_datatype->name, source_rank, (int) source_disp, source_count,
source_datatype->name, win->w_name);

sync = ompi_osc_rdma_module_sync_lookup (module, source_rank, &peer);
if (OPAL_UNLIKELY(NULL == sync)) {
return OMPI_ERR_RMA_SYNC;
}

return ompi_osc_rdma_get_w_req (sync, origin_addr, origin_count, origin_datatype, peer,
source_disp, source_count, source_datatype, NULL);
}

static inline int ompi_osc_rdma_get_w_req (ompi_osc_rdma_sync_t *sync, void *origin_addr, int origin_count, ompi_datatype_t *origin_datatype,
ompi_osc_rdma_peer_t *peer, ptrdiff_t source_disp, int source_count,
ompi_datatype_t *source_datatype, ompi_osc_rdma_request_t *request)
{
ompi_osc_rdma_module_t *module = sync->module;
mca_btl_base_registration_handle_t *source_handle;
uint64_t source_address;
ptrdiff_t source_span, source_lb;
int ret;

/* short-circuit case */
if (0 == origin_count || 0 == source_count) {
if (request) {
// 释放结构,直接返回
ompi_osc_rdma_request_complete (request, MPI_SUCCESS);
}

return OMPI_SUCCESS;
}

/* 计算 count 个数据类型在内存中的跨度。
* 此函数有助于为接收已键入的数据(例如用于 reduce 操作的数据)分配临时内存。
* 这个跨度是 count 数据类型的内存布局中最小和最大字节之间的距离,
* 换句话说,分配 count 所需的内存乘以数据类型,在开始和结束时没有间隙。
*/
source_span = opal_datatype_span(&source_datatype->super, source_count, &source_lb);

// 找到与内存区域关联的远程段,返回的是远端地址
ret = osc_rdma_get_remote_segment (module, peer, source_disp, source_span+source_lb,
&source_address, &source_handle);
if (OPAL_UNLIKELY(OMPI_SUCCESS != ret)) {
return ret;
}

/* optimize self/local communication */
if (ompi_osc_rdma_peer_local_base (peer)) {
return ompi_osc_rdma_copy_local ((void *) (intptr_t) source_address, source_count, source_datatype,
origin_addr, origin_count, origin_datatype, request);
}

return ompi_osc_rdma_master (sync, origin_addr, origin_count, origin_datatype, peer, source_address,
source_handle, source_count, source_datatype, request,
module->get_limit, ompi_osc_rdma_get_contig, true);
}

static inline int osc_rdma_get_remote_segment (ompi_osc_rdma_module_t *module, ompi_osc_rdma_peer_t *peer, ptrdiff_t target_disp,
size_t length, uint64_t *remote_address, mca_btl_base_registration_handle_t **remote_handle)
{
ompi_osc_rdma_region_t *region;
int ret;

if (MPI_WIN_FLAVOR_DYNAMIC == module->flavor) {
ret = ompi_osc_rdma_find_dynamic_region (module, peer, (uint64_t) target_disp, length, &region);
if (OMPI_SUCCESS != ret) {
OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_INFO, "could not retrieve region for %" PRIx64 " from window rank %d",
(uint64_t) target_disp, peer->rank);
return ret;
}

*remote_address = (uint64_t) target_disp;
*remote_handle = (mca_btl_base_registration_handle_t *) region->btl_handle_data;
} else {
ompi_osc_rdma_peer_extended_t *ex_peer = (ompi_osc_rdma_peer_extended_t *) peer;
int disp_unit = (module->same_disp_unit) ? module->disp_unit : ex_peer->disp_unit;
size_t size = (module->same_size) ? module->size : (size_t) ex_peer->size;

*remote_address = ex_peer->super.base + disp_unit * target_disp;
*remote_handle = ex_peer->super.base_handle;
}

return OMPI_SUCCESS;
}

int ompi_osc_rdma_find_dynamic_region (ompi_osc_rdma_module_t *module, ompi_osc_rdma_peer_t *peer, uint64_t base, size_t len,
ompi_osc_rdma_region_t **region)
{
ompi_osc_rdma_peer_dynamic_t *dy_peer = (ompi_osc_rdma_peer_dynamic_t *) peer;
intptr_t bound = (intptr_t) base + len;
ompi_osc_rdma_region_t *regions;
int ret = OMPI_SUCCESS, region_count;

OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_TRACE, "locating dynamic memory region matching: {%" PRIx64 ", %" PRIx64 "}"
" (len %lu)", base, base + len, (unsigned long) len);

OPAL_THREAD_LOCK(&module->lock);
// 需要看一些这个区域没有被加锁,获得一个排他锁,如果拿不到就一直循环等着
ompi_osc_rdma_lock_acquire_exclusive (module, peer, offsetof (ompi_osc_rdma_state_t, regions_lock));
// 这个区域不是本地的区域
if (!ompi_osc_rdma_peer_local_state (peer)) {
ret = ompi_osc_rdma_refresh_dynamic_region (module, dy_peer);
/* 此函数的作用是本地的远程进程视图与远程窗口的内容保持同步。
* 每次地址转换都会调用它,因为(当前)无法检测到附加区域是否已更改。
* 为了减少读取的数据量,我们首先读取区域计数(其中包含一个 id)。
* 如果这没有改变,则区域数据不会更新。
* 如果附加区域列表已更改,则从对等方读取所有有效区域,同时保持其区域锁定。
*/

if (OMPI_SUCCESS != ret) {
ompi_osc_rdma_lock_release_exclusive (module, peer, offsetof (ompi_osc_rdma_state_t, regions_lock));
return ret;
}

regions = dy_peer->regions;
region_count = dy_peer->region_count;
} else {
ompi_osc_rdma_state_t *peer_state = (ompi_osc_rdma_state_t *) peer->state;
regions = (ompi_osc_rdma_region_t *) peer_state->regions;
region_count = peer_state->region_count;
}

// 从排好序的regions里找到一个符合base地址+bound范围的块
// 使用二分法在0到region_count-1范围内找
*region = ompi_osc_rdma_find_region_containing (regions, 0, region_count - 1, (intptr_t) base, bound, module->region_size, NULL);
if (!*region) {
ret = OMPI_ERR_RMA_RANGE;
}
OPAL_THREAD_UNLOCK(&module->lock);
ompi_osc_rdma_lock_release_exclusive (module, peer, offsetof (ompi_osc_rdma_state_t, regions_lock));

/* round a matching region */
return ret;
}

根据这个要获得的区域在本地或者远端,分别调用两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
static int ompi_osc_rdma_copy_local (const void *source, int source_count, ompi_datatype_t *source_datatype,
void *target, int target_count, ompi_datatype_t *target_datatype,
ompi_osc_rdma_request_t *request)
{
int ret;

OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_TRACE, "performing local copy from %p -> %p", source, target);

opal_atomic_mb ();
ret = ompi_datatype_sndrcv (source, source_count, source_datatype, target, target_count, target_datatype);
// 处理pack和unpack,或者直接复制
if (request) {
ompi_osc_rdma_request_complete (request, ret);
}

return ret;
}

static inline int ompi_osc_rdma_master (ompi_osc_rdma_sync_t *sync, void *local_address, int local_count,
ompi_datatype_t *local_datatype, ompi_osc_rdma_peer_t *peer,
uint64_t remote_address, mca_btl_base_registration_handle_t *remote_handle,
int remote_count, ompi_datatype_t *remote_datatype,
ompi_osc_rdma_request_t *request, const size_t max_rdma_len,
const ompi_osc_rdma_fn_t rdma_fn, const bool alloc_reqs)
{
size_t rdma_len;
ptrdiff_t lb, extent;
int ret;

rdma_len = local_datatype->super.size * local_count;

/* fast path for contiguous rdma */
if (OPAL_LIKELY(ompi_datatype_is_contiguous_memory_layout (local_datatype, local_count) &&
ompi_datatype_is_contiguous_memory_layout (remote_datatype, remote_count) &&
rdma_len <= max_rdma_len)) {
if (NULL == request && alloc_reqs) {
ompi_osc_rdma_module_t *module = sync->module;
OMPI_OSC_RDMA_REQUEST_ALLOC(module, peer, request);
request->internal = true;
request->type = OMPI_OSC_RDMA_TYPE_RDMA;
}

/* ignore failure here */
(void) ompi_datatype_get_true_extent (local_datatype, &lb, &extent);
local_address = (void *)((intptr_t) local_address + lb);

(void) ompi_datatype_get_true_extent (remote_datatype, &lb, &extent);
remote_address += lb;

OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_TRACE, "performing rdma on contiguous region. local: %p, "
"remote: 0x%lx, length: %lu", local_address, (unsigned long) remote_address,
rdma_len);

do {
ret = rdma_fn (sync, peer, remote_address, remote_handle, local_address, rdma_len, request);
if (OPAL_LIKELY(OPAL_SUCCESS == ret)) {
return OMPI_SUCCESS;
}

ompi_osc_rdma_progress (sync->module);
} while (1);
}

return ompi_osc_rdma_master_noncontig (sync, local_address, local_count, local_datatype, peer, remote_address,
remote_handle, remote_count, remote_datatype, request,
max_rdma_len, rdma_fn, alloc_reqs);
}

/**
* @brief 将 rdma 事务分解为连续区域
*
* @param[in] local_address base of local region (source for put, destination for get)
* @param[in] local_count number of elements in local region
* @param[in] local_datatype datatype of local region
* @param[in] peer peer object for remote peer
* @param[in] remote_address base of remote region (destination for put, source for get)
* @param[in] remote_handle btl registration handle for remote region (must be valid for the entire region)
* @param[in] remote_count number of elements in remote region
* @param[in] remote_datatype datatype of remote region
* @param[in] module osc rdma module
* @param[in] request osc rdma request if used (can be NULL)
* @param[in] max_rdma_len maximum length of an rdma request (usually btl limitation)
* @param[in] rdma_fn function to use for contiguous rdma operations
* @param[in] alloc_reqs true if rdma_fn requires a valid request object (any allocated objects will be marked internal)
*
* This function does the work of breaking a non-contiguous rdma transfer into contiguous components. It will
* continue to submit rdma transfers until the entire region is transferred or a fatal error occurs.
*/
static int ompi_osc_rdma_master_noncontig (ompi_osc_rdma_sync_t *sync, void *local_address, int local_count, ompi_datatype_t *local_datatype,
ompi_osc_rdma_peer_t *peer, uint64_t remote_address,
mca_btl_base_registration_handle_t *remote_handle, int remote_count,
ompi_datatype_t *remote_datatype, ompi_osc_rdma_request_t *request, const size_t max_rdma_len,
const ompi_osc_rdma_fn_t rdma_fn, const bool alloc_reqs)
{
ompi_osc_rdma_module_t *module = sync->module;
struct iovec local_iovec[OMPI_OSC_RDMA_DECODE_MAX], remote_iovec[OMPI_OSC_RDMA_DECODE_MAX];
opal_convertor_t local_convertor, remote_convertor;
uint32_t local_iov_count, remote_iov_count;
uint32_t local_iov_index, remote_iov_index;
/* needed for opal_convertor_raw but not used */
size_t local_size, remote_size, rdma_len;
ompi_osc_rdma_request_t *subreq;
int ret;
bool done;

subreq = NULL;

OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_TRACE, "scheduling rdma on non-contiguous datatype(s) or large region");

/* prepare convertors for the source and target. these convertors will be used to determine the
* contiguous segments within the source and target. */
OBJ_CONSTRUCT(&remote_convertor, opal_convertor_t);
ret = opal_convertor_copy_and_prepare_for_send (ompi_mpi_local_convertor, &remote_datatype->super, remote_count,
(void *) (intptr_t) remote_address, 0, &remote_convertor);
if (OPAL_UNLIKELY(OMPI_SUCCESS != ret)) {
return ret;
}

OBJ_CONSTRUCT(&local_convertor, opal_convertor_t);
ret = opal_convertor_copy_and_prepare_for_send (ompi_mpi_local_convertor, &local_datatype->super, local_count,
local_address, 0, &local_convertor);
if (OPAL_UNLIKELY(OMPI_SUCCESS != ret)) {
return ret;
}
// 以上是转换器转换压缩

if (request) {
/* keep the request from completing until all the transfers have started */
request->outstanding_requests = 1;
}

local_iov_index = 0;
local_iov_count = 0;

do {
/* decode segments of the remote data */
remote_iov_count = OMPI_OSC_RDMA_DECODE_MAX;
remote_iov_index = 0;

/* opal_convertor_raw returns true when it has reached the end of the data */
done = opal_convertor_raw (&remote_convertor, remote_iovec, &remote_iov_count, &remote_size);

/* loop on the target segments until we have exhaused the decoded source data */
while (remote_iov_index != remote_iov_count) {
if (local_iov_index == local_iov_count) {
/* decode segments of the target buffer */
local_iov_count = OMPI_OSC_RDMA_DECODE_MAX;
local_iov_index = 0;
(void) opal_convertor_raw (&local_convertor, local_iovec, &local_iov_count, &local_size);
}

/* we already checked that the target was large enough. this should be impossible */
assert (0 != local_iov_count);

/* determine how much to transfer in this operation */
rdma_len = opal_min(opal_min(local_iovec[local_iov_index].iov_len, remote_iovec[remote_iov_index].iov_len), max_rdma_len);

/* execute the get */
if (!subreq && alloc_reqs) {
OMPI_OSC_RDMA_REQUEST_ALLOC(module, peer, subreq);
subreq->internal = true;
subreq->type = OMPI_OSC_RDMA_TYPE_RDMA;
subreq->parent_request = request;

if (request) {
(void) OPAL_THREAD_ADD_FETCH32 (&request->outstanding_requests, 1);
}
} else if (!alloc_reqs) {
subreq = request;
}

ret = rdma_fn (sync, peer, (uint64_t) (intptr_t) remote_iovec[remote_iov_index].iov_base, remote_handle,
local_iovec[local_iov_index].iov_base, rdma_len, subreq);
if (OPAL_UNLIKELY(OMPI_SUCCESS != ret)) {
if (OPAL_UNLIKELY(OMPI_ERR_OUT_OF_RESOURCE != ret)) {
if (request) {
ompi_osc_rdma_request_deref (request);
}

if (alloc_reqs) {
OMPI_OSC_RDMA_REQUEST_RETURN(subreq);
}

/* something bad happened. need to figure out best way to handle rma errors */
return ret;
}

/* progress and try again */
ompi_osc_rdma_progress (module);
continue;
}
subreq = NULL;

/* adjust io vectors */
local_iovec[local_iov_index].iov_len -= rdma_len;
remote_iovec[remote_iov_index].iov_len -= rdma_len;
local_iovec[local_iov_index].iov_base = (void *)((intptr_t) local_iovec[local_iov_index].iov_base + rdma_len);
remote_iovec[remote_iov_index].iov_base = (void *)((intptr_t) remote_iovec[remote_iov_index].iov_base + rdma_len);

local_iov_index += (0 == local_iovec[local_iov_index].iov_len);
remote_iov_index += (0 == remote_iovec[remote_iov_index].iov_len);
}
} while (!done);

if (request) {
/* release our reference so the request can complete */
ompi_osc_rdma_request_deref (request);
}

OSC_RDMA_VERBOSE(MCA_BASE_VERBOSE_TRACE, "finished scheduling rdma on non-contiguous datatype(s)");

/* clean up convertors */
opal_convertor_cleanup (&local_convertor);
OBJ_DESTRUCT(&local_convertor);
opal_convertor_cleanup (&remote_convertor);
OBJ_DESTRUCT(&remote_convertor);

return OMPI_SUCCESS;
}

UCX

因为在之前的报错里看到过UCX的字样,所以跟了一下mca_pml_ucx_send函数,底层是用了Unified Communication X库。先是找到代表dst进程的endpoint,再用两个函数实现send。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int mca_pml_ucx_send(const void *buf, size_t count, ompi_datatype_t *datatype, int dst,
int tag, mca_pml_base_send_mode_t mode,
struct ompi_communicator_t* comm)
{
ucp_ep_h ep;

PML_UCX_TRACE_SEND("%s", buf, count, datatype, dst, tag, mode, comm,
mode == MCA_PML_BASE_SEND_BUFFERED ? "bsend" : "send");

ep = mca_pml_ucx_get_ep(comm, dst);
if (OPAL_UNLIKELY(NULL == ep)) {
return OMPI_ERROR;
}

#if SPC_ENABLE == 1
size_t dt_size;
ompi_datatype_type_size(datatype, &dt_size);
SPC_USER_OR_MPI(tag, dt_size*count,
OMPI_SPC_BYTES_SENT_USER, OMPI_SPC_BYTES_SENT_MPI);
#endif

#if HAVE_DECL_UCP_TAG_SEND_NBR
if (OPAL_LIKELY((MCA_PML_BASE_SEND_BUFFERED != mode) &&
(MCA_PML_BASE_SEND_SYNCHRONOUS != mode))) {
return mca_pml_ucx_send_nbr(ep, buf, count, datatype,
PML_UCX_MAKE_SEND_TAG(tag, comm));
}
#endif

return mca_pml_ucx_send_nb(ep, buf, count, datatype,
mca_pml_ucx_get_datatype(datatype),
PML_UCX_MAKE_SEND_TAG(tag, comm), mode);
}

实现send的其中一个非阻塞函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#if HAVE_DECL_UCP_TAG_SEND_NBR
static inline __opal_attribute_always_inline__ int
mca_pml_ucx_send_nbr(ucp_ep_h ep, const void *buf, size_t count,
ompi_datatype_t *datatype, ucp_tag_t tag)
{
/* coverity[bad_alloc_arithmetic] */
ucs_status_ptr_t req = PML_UCX_REQ_ALLOCA();
#if HAVE_DECL_UCP_TAG_SEND_NBX
pml_ucx_datatype_t *op_data = mca_pml_ucx_get_op_data(datatype);
ucp_request_param_t param = {
.op_attr_mask = UCP_OP_ATTR_FIELD_REQUEST |
(op_data->op_param.send.op_attr_mask & UCP_OP_ATTR_FIELD_DATATYPE) |
UCP_OP_ATTR_FLAG_FAST_CMPL,
.datatype = op_data->op_param.send.datatype,
.request = req
};

// ucp_tag_send_nb和ucp_tag_send_nbx可能是一样的,因为手册里没有找到ucp_tag_send_nbx,所以可能是不同版本
// 此例程将由本地地址缓冲区、大小计数和数据类型对象描述的消息发送到目标端点 ep。
// 每条消息都与一个标签值相关联,该标签值用于在接收器上进行消息匹配。
// 该例程是非阻塞的,因此会立即返回,但是实际的发送操作可能会延迟。 当可以安全地重用源缓冲区时,发送操作被认为已完成。
// 如果发送操作立即完成,则例程返回 UCS_OK 并且不调用回调函数 cb。
// 如果操作没有立即完成并且没有报告错误,那么 UCP 库将安排在发送操作完成时调用回调 cb。
// 所以这里没有wait,而且检测到错误就返回了

req = ucp_tag_send_nbx(ep, buf,
mca_pml_ucx_get_data_size(op_data, count),
tag, &param);
if (OPAL_LIKELY(req == UCS_OK)) {
return OMPI_SUCCESS;
} else if (UCS_PTR_IS_ERR(req)) {
PML_UCX_ERROR("%s failed: %d, %s", __func__, UCS_PTR_STATUS(req),
ucs_status_string(UCS_PTR_STATUS(req)));
return OPAL_ERROR;
}
#else
ucs_status_t status;
status = ucp_tag_send_nbr(ep, buf, count,
mca_pml_ucx_get_datatype(datatype), tag, req);
if (OPAL_LIKELY(status == UCS_OK)) {
return OMPI_SUCCESS;
}
/* 此例程提供了一种方便且有效的方式来实现阻塞发送模式。它还比 ucp_tag_send_nbr() 更快地完成请求,因为:
* 它总是使用 uct_ep_am_bcopy() 将数据发送到集合阈值。
* 它的集合阈值高于 ucp_tag_send_nb() 使用的阈值。阈值由 UCX_SEND_NBR_RNDV_THRESH 环境变量控制。
* 它的请求处理更简单。没有回调,也不需要分配和释放请求。事实上,请求可以由调用者在堆栈上分配。
* 此例程将由本地地址缓冲区、大小计数和数据类型对象描述的消息发送到目标端点 ep。每条消息都与一个标签值相关联,该标签值用于在接收器上进行消息匹配。
* 该例程是非阻塞的,因此会立即返回,但是实际的发送操作可能会延迟。当可以安全地重用源缓冲区时,发送操作被认为已完成。如果发送操作立即完成,则例程返回 UCS_OK。

* 如果操作没有立即完成并且没有报告错误,那么 UCP 库将填充用户提供的请求并返回 UCS_INPROGRESS 状态。为了监控操作的完成,应该使用 ucp_request_check_status()。
*/
#endif

MCA_COMMON_UCX_WAIT_LOOP(req, ompi_pml_ucx.ucp_worker, "ucx send nbr", (void)0);
}
#endif

实现send的其中一个阻塞函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static inline __opal_attribute_always_inline__ int
mca_pml_ucx_send_nb(ucp_ep_h ep, const void *buf, size_t count,
ompi_datatype_t *datatype, ucp_datatype_t ucx_datatype,
ucp_tag_t tag, mca_pml_base_send_mode_t mode)
{
ompi_request_t *req;

req = (ompi_request_t*)mca_pml_ucx_common_send(ep, buf, count, datatype,
mca_pml_ucx_get_datatype(datatype),
tag, mode,
mca_pml_ucx_send_completion_empty);
// 应该是发送完之后一直等待,直到结束,因为有wait loop
if (OPAL_LIKELY(req == NULL)) {
return OMPI_SUCCESS;
} else if (!UCS_PTR_IS_ERR(req)) {
PML_UCX_VERBOSE(8, "got request %p", (void*)req);
MCA_COMMON_UCX_WAIT_LOOP(req, ompi_pml_ucx.ucp_worker, "ucx send", ucp_request_free(req));
} else {
PML_UCX_ERROR("ucx send failed: %s", ucs_status_string(UCS_PTR_STATUS(req)));
return OMPI_ERROR;
}
}

mca_pml_ucx_common_send根据mode调用三种函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
static inline ucs_status_ptr_t mca_pml_ucx_common_send(ucp_ep_h ep, const void *buf,
size_t count,
ompi_datatype_t *datatype,
ucp_datatype_t ucx_datatype,
ucp_tag_t tag,
mca_pml_base_send_mode_t mode,
ucp_send_callback_t cb)
{
if (OPAL_UNLIKELY(MCA_PML_BASE_SEND_BUFFERED == mode)) {
return mca_pml_ucx_bsend(ep, buf, count, datatype, tag);
} else if (OPAL_UNLIKELY(MCA_PML_BASE_SEND_SYNCHRONOUS == mode)) {
return ucp_tag_send_sync_nb(ep, buf, count, ucx_datatype, tag, cb);
} else {
return ucp_tag_send_nb(ep, buf, count, ucx_datatype, tag, cb);
}
// ucp_tag_send_nb将由本地地址缓冲区、大小计数和数据类型对象描述的消息发送到目标端点 ep。
// 每条消息都与一个标签值相关联,该标签值用于在接收器上进行消息匹配。
// 该例程是非阻塞的,因此会立即返回,但是实际的发送操作可能会延迟。
// 当可以安全地重用源缓冲区时,发送操作被认为已完成。 如果发送操作立即完成,则例程返回 UCS_OK 并且不调用回调函数 cb。
// 如果操作没有立即完成并且没有报告错误,那么 UCP 库将安排在发送操作完成时调用回调 cb。 换句话说,消息的完成可以通过返回码或回调来表示。

// ucp_tag_send_sync_nb 与 ucp_tag_send_nb 相同,除了请求仅在消息上存在远程标记匹配后完成(这并不总是意味着远程接收已完成)。
// 这个函数永远不会“就地”完成,并且总是返回一个请求句柄。
}


static ucs_status_ptr_t
mca_pml_ucx_bsend(ucp_ep_h ep, const void *buf, size_t count,
ompi_datatype_t *datatype, uint64_t pml_tag)
{
ompi_request_t *req;
void *packed_data;
size_t packed_length;
size_t offset;
uint32_t iov_count;
struct iovec iov;
opal_convertor_t opal_conv;

OBJ_CONSTRUCT(&opal_conv, opal_convertor_t);
opal_convertor_copy_and_prepare_for_send(ompi_proc_local_proc->super.proc_convertor,
&datatype->super, count, buf, 0,
&opal_conv);
// 设置convertor的fAdvance

opal_convertor_get_packed_size(&opal_conv, &packed_length);

packed_data = mca_pml_base_bsend_request_alloc_buf(packed_length);
// 分配空间

if (OPAL_UNLIKELY(NULL == packed_data)) {
OBJ_DESTRUCT(&opal_conv);
PML_UCX_ERROR("bsend: failed to allocate buffer");
return UCS_STATUS_PTR(OMPI_ERROR);
}

iov_count = 1;
iov.iov_base = packed_data;
iov.iov_len = packed_length;

PML_UCX_VERBOSE(8, "bsend of packed buffer %p len %zu", packed_data, packed_length);
offset = 0;
opal_convertor_set_position(&opal_conv, &offset);
if (0 > opal_convertor_pack(&opal_conv, &iov, &iov_count, &packed_length)) {
// 获取到指针后使用memcpy
mca_pml_base_bsend_request_free(packed_data); // 释放request
OBJ_DESTRUCT(&opal_conv);
PML_UCX_ERROR("bsend: failed to pack user datatype");
return UCS_STATUS_PTR(OMPI_ERROR);
}

OBJ_DESTRUCT(&opal_conv);

req = (ompi_request_t*)ucp_tag_send_nb(ep, packed_data, packed_length,
ucp_dt_make_contig(1), pml_tag,
mca_pml_ucx_bsend_completion);
if (NULL == req) {
/* request was completed in place */
mca_pml_base_bsend_request_free(packed_data);
return NULL;
}

if (OPAL_UNLIKELY(UCS_PTR_IS_ERR(req))) {
mca_pml_base_bsend_request_free(packed_data);
PML_UCX_ERROR("ucx bsend failed: %s", ucs_status_string(UCS_PTR_STATUS(req)));
return UCS_STATUS_PTR(OMPI_ERROR);
}

req->req_complete_cb_data = packed_data;
return NULL;
}

convertor

多次看到,可能是在不同架构下进行传输的转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

struct opal_convertor_t {
opal_object_t super; /**< basic superclass */
uint32_t remoteArch; /**< the remote architecture */
uint32_t flags; /**< the properties of this convertor */
size_t local_size; /**< overall length data on local machine, compared to bConverted */
size_t remote_size; /**< overall length data on remote machine, compared to bConverted */
const opal_datatype_t *pDesc; /**< the datatype description associated with the convertor */
const dt_type_desc_t *use_desc; /**< the version used by the convertor (normal or optimized) */
opal_datatype_count_t count; /**< the total number of full datatype elements */

/* --- cacheline boundary (64 bytes - if 64bits arch and !OPAL_ENABLE_DEBUG) --- */
uint32_t stack_size; /**< size of the allocated stack */
unsigned char *pBaseBuf; /**< initial buffer as supplied by the user */
dt_stack_t *pStack; /**< the local stack for the actual conversion */
convertor_advance_fct_t fAdvance; /**< pointer to the pack/unpack functions */

/* --- cacheline boundary (96 bytes - if 64bits arch and !OPAL_ENABLE_DEBUG) --- */
struct opal_convertor_master_t *master; /**< the master convertor */

/* All others fields get modified for every call to pack/unpack functions */
uint32_t stack_pos; /**< the actual position on the stack */
size_t partial_length; /**< amount of data left over from the last unpack */
size_t bConverted; /**< # of bytes already converted */

/* --- cacheline boundary (128 bytes - if 64bits arch and !OPAL_ENABLE_DEBUG) --- */
uint32_t checksum; /**< checksum computed by pack/unpack operation */
uint32_t csum_ui1; /**< partial checksum computed by pack/unpack operation */
size_t csum_ui2; /**< partial checksum computed by pack/unpack operation */

/* --- fields are no more aligned on cacheline --- */
dt_stack_t static_stack[DT_STATIC_STACK_SIZE]; /**< local stack for small datatypes */

#if OPAL_CUDA_SUPPORT
memcpy_fct_t cbmemcpy; /**< memcpy or cuMemcpy */
void *stream; /**< CUstream for async copy */
#endif
};

这是pack和unpack,应该是用于通信的时候数据压缩的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* Return 0 if everything went OK and if there is still room before the complete
* conversion of the data (need additional call with others input buffers )
* 1 if everything went fine and the data was completly converted
* -1 something wrong occurs.
*/
int32_t opal_convertor_pack(opal_convertor_t *pConv, struct iovec *iov, uint32_t *out_size,
size_t *max_data)
{
OPAL_CONVERTOR_SET_STATUS_BEFORE_PACK_UNPACK(pConv, iov, out_size, max_data);

if (OPAL_LIKELY(pConv->flags & CONVERTOR_NO_OP)) {
/**
* We are doing conversion on a contiguous datatype on a homogeneous
* environment. The convertor contain minimal information, we only
* use the bConverted to manage the conversion.
*/
uint32_t i;
unsigned char *base_pointer;
size_t pending_length = pConv->local_size - pConv->bConverted;

*max_data = pending_length;
opal_convertor_get_current_pointer(pConv, (void **) &base_pointer);

for (i = 0; i < *out_size; i++) {
if (iov[i].iov_len >= pending_length) {
goto complete_contiguous_data_pack;
}
if (OPAL_LIKELY(NULL == iov[i].iov_base)) {
iov[i].iov_base = (IOVBASE_TYPE *) base_pointer;
} else {
#if OPAL_CUDA_SUPPORT
MEMCPY_CUDA(iov[i].iov_base, base_pointer, iov[i].iov_len, pConv);
#else
MEMCPY(iov[i].iov_base, base_pointer, iov[i].iov_len);
#endif
}
pending_length -= iov[i].iov_len;
base_pointer += iov[i].iov_len;
}
*max_data -= pending_length;
pConv->bConverted += (*max_data);
return 0;

complete_contiguous_data_pack:
iov[i].iov_len = pending_length;
if (OPAL_LIKELY(NULL == iov[i].iov_base)) {
iov[i].iov_base = (IOVBASE_TYPE *) base_pointer;
} else {
#if OPAL_CUDA_SUPPORT
MEMCPY_CUDA(iov[i].iov_base, base_pointer, iov[i].iov_len, pConv);
#else
MEMCPY(iov[i].iov_base, base_pointer, iov[i].iov_len);
#endif
}
pConv->bConverted = pConv->local_size;
*out_size = i + 1;
pConv->flags |= CONVERTOR_COMPLETED;
return 1;
}

return pConv->fAdvance(pConv, iov, out_size, max_data);
}

int32_t opal_convertor_unpack(opal_convertor_t *pConv, struct iovec *iov, uint32_t *out_size,
size_t *max_data)
{
OPAL_CONVERTOR_SET_STATUS_BEFORE_PACK_UNPACK(pConv, iov, out_size, max_data);

if (OPAL_LIKELY(pConv->flags & CONVERTOR_NO_OP)) {
/**
* 我们正在同构环境中对连续数据类型进行转换。 转换器包含最少的信息,我们只使用 bConverted 来管理转换。
*/
uint32_t i;
unsigned char *base_pointer;
size_t pending_length = pConv->local_size - pConv->bConverted;

*max_data = pending_length;
opal_convertor_get_current_pointer(pConv, (void **) &base_pointer);

for (i = 0; i < *out_size; i++) {
if (iov[i].iov_len >= pending_length) {
goto complete_contiguous_data_unpack;
}
#if OPAL_CUDA_SUPPORT
MEMCPY_CUDA(base_pointer, iov[i].iov_base, iov[i].iov_len, pConv);
#else
MEMCPY(base_pointer, iov[i].iov_base, iov[i].iov_len);
#endif
pending_length -= iov[i].iov_len;
base_pointer += iov[i].iov_len;
}
*max_data -= pending_length;
pConv->bConverted += (*max_data);
return 0;

complete_contiguous_data_unpack:
iov[i].iov_len = pending_length;
#if OPAL_CUDA_SUPPORT
MEMCPY_CUDA(base_pointer, iov[i].iov_base, iov[i].iov_len, pConv);
#else
MEMCPY(base_pointer, iov[i].iov_base, iov[i].iov_len);
#endif
pConv->bConverted = pConv->local_size;
*out_size = i + 1;
pConv->flags |= CONVERTOR_COMPLETED;
return 1;
}

return pConv->fAdvance(pConv, iov, out_size, max_data);
}

用于在执行通信的时候进行准备,主要是设置fAdvance这个函数,用在上边的pack和unpack里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int32_t opal_convertor_prepare_for_send(opal_convertor_t *convertor,
const struct opal_datatype_t *datatype, size_t count,
const void *pUserBuf)
{
convertor->flags |= CONVERTOR_SEND;
#if OPAL_CUDA_SUPPORT
if (!(convertor->flags & CONVERTOR_SKIP_CUDA_INIT)) {
mca_cuda_convertor_init(convertor, pUserBuf);
}
#endif

OPAL_CONVERTOR_PREPARE(convertor, datatype, count, pUserBuf);

#if defined(CHECKSUM)
if (convertor->flags & CONVERTOR_WITH_CHECKSUM) {
if (CONVERTOR_SEND_CONVERSION
== (convertor->flags & (CONVERTOR_SEND_CONVERSION | CONVERTOR_HOMOGENEOUS))) {
convertor->fAdvance = opal_pack_general_checksum;
} else {
if (datatype->flags & OPAL_DATATYPE_FLAG_CONTIGUOUS) {
if (((datatype->ub - datatype->lb) == (ptrdiff_t) datatype->size)
|| (1 >= convertor->count)) {
convertor->fAdvance = opal_pack_homogeneous_contig_checksum; // 都是计算checksum的函数,例如crc码
} else {
convertor->fAdvance = opal_pack_homogeneous_contig_with_gaps_checksum;
}
} else {
convertor->fAdvance = opal_generic_simple_pack_checksum;
}
}
} else {
#endif /* defined(CHECKSUM) */
if (CONVERTOR_SEND_CONVERSION
== (convertor->flags & (CONVERTOR_SEND_CONVERSION | CONVERTOR_HOMOGENEOUS))) {
convertor->fAdvance = opal_pack_general;
} else {
if (datatype->flags & OPAL_DATATYPE_FLAG_CONTIGUOUS) {
if (((datatype->ub - datatype->lb) == (ptrdiff_t) datatype->size)
|| (1 >= convertor->count)) {
convertor->fAdvance = opal_pack_homogeneous_contig;
} else {
convertor->fAdvance = opal_pack_homogeneous_contig_with_gaps;
}
} else {
convertor->fAdvance = opal_generic_simple_pack;
}
}
#if defined(CHECKSUM)
}
#endif
return OPAL_SUCCESS;
}

MPI_Recv

recv和send类似,最后都是调用pml_recv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int MPI_Recv(void *buf, int count, MPI_Datatype type, int source,
int tag, MPI_Comm comm, MPI_Status *status)
{
int rc = MPI_SUCCESS;

SPC_RECORD(OMPI_SPC_RECV, 1);

MEMCHECKER(
memchecker_datatype(type);
memchecker_call(&opal_memchecker_base_isaddressable, buf, count, type);
memchecker_comm(comm);
);

if ( MPI_PARAM_CHECK ) {
OMPI_ERR_INIT_FINALIZE(FUNC_NAME);
OMPI_CHECK_DATATYPE_FOR_RECV(rc, type, count);
OMPI_CHECK_USER_BUFFER(rc, buf, type, count);

if (ompi_comm_invalid(comm)) {
return OMPI_ERRHANDLER_NOHANDLE_INVOKE(MPI_ERR_COMM, FUNC_NAME);
} else if (((tag < 0) && (tag != MPI_ANY_TAG)) || (tag > mca_pml.pml_max_tag)) {
rc = MPI_ERR_TAG;
} else if ((source != MPI_ANY_SOURCE) &&
(MPI_PROC_NULL != source) &&
ompi_comm_peer_invalid(comm, source)) {
rc = MPI_ERR_RANK;
}

OMPI_ERRHANDLER_CHECK(rc, comm, rc, FUNC_NAME);
}

// fault tolerance ......

if (MPI_PROC_NULL == source) {
if (MPI_STATUS_IGNORE != status) {
OMPI_COPY_STATUS(status, ompi_request_empty.req_status, false);
}
return MPI_SUCCESS;
}

rc = MCA_PML_CALL(recv(buf, count, type, source, tag, comm, status));
OMPI_ERRHANDLER_RETURN(rc, comm, rc, FUNC_NAME);
}

pml_recv主要有以下几个:

1
2
3
4
5
mca_pml_cm_recv
mca_pml_ob1_recv
mca_pml_ucx_recv
mca_spml_ucx_recv
mca_pml_monitoring_recv

主要还是跟mca_pml_cm_recv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
__opal_attribute_always_inline__ static inline int
mca_pml_cm_recv(void *addr,
size_t count,
ompi_datatype_t * datatype,
int src,
int tag,
struct ompi_communicator_t *comm,
ompi_status_public_t * status)
{
int ret;
uint32_t flags = 0;
#if OPAL_ENABLE_HETEROGENEOUS_SUPPORT
ompi_proc_t *ompi_proc;
#endif
opal_convertor_t convertor;
mca_pml_cm_request_t req;
mca_mtl_request_t *req_mtl = alloca(sizeof(mca_mtl_request_t) + ompi_mtl->mtl_request_size);

OBJ_CONSTRUCT(&convertor, opal_convertor_t);
req_mtl->ompi_req = &req.req_ompi;
req_mtl->completion_callback = mca_pml_cm_recv_fast_completion;

req.req_pml_type = MCA_PML_CM_REQUEST_RECV_THIN;
req.req_free_called = false;
req.req_ompi.req_complete = false;
req.req_ompi.req_complete_cb = NULL;
req.req_ompi.req_state = OMPI_REQUEST_ACTIVE;
req.req_ompi.req_status.MPI_TAG = OMPI_ANY_TAG;
req.req_ompi.req_status.MPI_ERROR = OMPI_SUCCESS;
req.req_ompi.req_status._cancelled = 0;

#if OPAL_ENABLE_HETEROGENEOUS_SUPPORT
if( MPI_ANY_SOURCE == src ) {
ompi_proc = ompi_proc_local_proc;
} else {
ompi_proc = ompi_comm_peer_lookup( comm, src );
}

MCA_PML_CM_SWITCH_CUDA_CONVERTOR_OFF(flags, datatype, count);

opal_convertor_copy_and_prepare_for_recv(
ompi_proc->super.proc_convertor,
&(datatype->super),
count,
addr,
flags,
&convertor );
#else
MCA_PML_CM_SWITCH_CUDA_CONVERTOR_OFF(flags, datatype, count);

opal_convertor_copy_and_prepare_for_recv(
ompi_mpi_local_convertor,
&(datatype->super),
count,
addr,
flags,
&convertor );
#endif

ret = OMPI_MTL_CALL(irecv(ompi_mtl,
comm,
src,
tag,
&convertor,
req_mtl));
if( OPAL_UNLIKELY(OMPI_SUCCESS != ret) ) {
OBJ_DESTRUCT(&convertor);
return ret;
}

ompi_request_wait_completion(&req.req_ompi);

if (MPI_STATUS_IGNORE != status) {
OMPI_COPY_STATUS(status, req.req_ompi.req_status, false);
}
ret = req.req_ompi.req_status.MPI_ERROR;
OBJ_DESTRUCT(&convertor);
return ret;
}

MPI_Allgather

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
int MPI_Allgather(const void *sendbuf, int sendcount, MPI_Datatype sendtype,
void *recvbuf, int recvcount, MPI_Datatype recvtype,
MPI_Comm comm)
{
int err;

SPC_RECORD(OMPI_SPC_ALLGATHER, 1);

MEMCHECKER(
int rank;
ptrdiff_t ext;

rank = ompi_comm_rank(comm);
ompi_datatype_type_extent(recvtype, &ext);

memchecker_datatype(recvtype);
memchecker_comm(comm);
/* 检查发送缓冲区是否合法. */
if (MPI_IN_PLACE == sendbuf) {
memchecker_call(&opal_memchecker_base_isdefined,
(char *)(recvbuf)+rank*recvcount*ext,
recvcount, recvtype);
} else {
memchecker_datatype(sendtype);
memchecker_call(&opal_memchecker_base_isdefined, sendbuf, sendcount, sendtype);
}
/* check whether the receive buffer is addressable. */
memchecker_call(&opal_memchecker_base_isaddressable, recvbuf, recvcount, recvtype);
);

if (MPI_PARAM_CHECK) {
err = MPI_SUCCESS;
OMPI_ERR_INIT_FINALIZE(FUNC_NAME);
if (ompi_comm_invalid(comm)) {
OMPI_ERRHANDLER_NOHANDLE_INVOKE(MPI_ERR_COMM, FUNC_NAME);
} else if (MPI_DATATYPE_NULL == recvtype || NULL == recvtype) {
err = MPI_ERR_TYPE;
} else if (recvcount < 0) {
err = MPI_ERR_COUNT;
} else if ((MPI_IN_PLACE == sendbuf && OMPI_COMM_IS_INTER(comm)) ||
MPI_IN_PLACE == recvbuf) {
return OMPI_ERRHANDLER_INVOKE(comm, MPI_ERR_ARG, FUNC_NAME);
} else if (MPI_IN_PLACE != sendbuf) {
OMPI_CHECK_DATATYPE_FOR_SEND(err, sendtype, sendcount);
}
OMPI_ERRHANDLER_CHECK(err, comm, err, FUNC_NAME);
}

/* 每个进程都必须给出相同的发送签名,这意味着如果有任何东西要发送用于内部通信器案例,每个人都必须给出一个 sendcount > 0。
* 但是,如果我们正在执行 IN_PLACE,请检查 recvcount,而不是 sendcount。
*/
if ( OMPI_COMM_IS_INTRA(comm) ) {
if ((MPI_IN_PLACE != sendbuf && 0 == sendcount) ||
(0 == recvcount)) {
return MPI_SUCCESS;
}
}
else if ( OMPI_COMM_IS_INTER(comm) ){
/* 对于inter的通信器,通信模式不必是对称的。 具体来说,一组允许 sendcount=0,而另一组有一个有效的 sendcount。 因此,不做任何事情的唯一方法是如果 sendcount 和 recvcount 都为零 */
if ( 0 == sendcount && 0 == recvcount ) {
return MPI_SUCCESS;
}
}

/* Invoke the coll component to perform the back-end operation */

err = comm->c_coll->coll_allgather(sendbuf, sendcount, sendtype,
recvbuf, recvcount, recvtype, comm,
comm->c_coll->coll_allgather_module);
OMPI_ERRHANDLER_RETURN(err, comm, err, FUNC_NAME);
}

coll_allgather有如下几个实现:

1
2
3
4
5
mca_coll_basic_allgather_inter
ompi_coll_base_allgather_intra_basic_linear
mca_coll_demo_allgather_intra
mca_coll_demo_allgather_inter
mca_coll_self_allgather_intra

mca_coll_basic_allgather_inter实现,应该是在两个域之间实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
int mca_coll_basic_allgather_inter(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void *rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int rank, root = 0, size, rsize, err, i, line;
char *tmpbuf_free = NULL, *tmpbuf, *ptmp;
ptrdiff_t rlb, rextent, incr;
ptrdiff_t gap, span;
ompi_request_t *req;
ompi_request_t **reqs = NULL;

rank = ompi_comm_rank(comm);
size = ompi_comm_size(comm);
rsize = ompi_comm_remote_size(comm);

/* Algorithm:
* - gather操作,聚集到远程组中的根(同时执行,这就是我们不能使用 coll_gather 的原因)。
* - 在两个根之间交换温度结果
* - 进程间广播(再次同时)。
*/

/* Step one: gather operations: */
if (rank != root) {
/* 把自己的数据发送给根进程 */
err = MCA_PML_CALL(send(sbuf, scount, sdtype, root,
MCA_COLL_BASE_TAG_ALLGATHER,
MCA_PML_BASE_SEND_STANDARD, comm));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }
} else {
/* receive a msg. from all other procs. */
err = ompi_datatype_get_extent(rdtype, &rlb, &rextent);
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

/* 初始化request数组 */
reqs = ompi_coll_base_comm_get_reqs(module->base_data, rsize + 1);
if( NULL == reqs ) { line = __LINE__; err = OMPI_ERR_OUT_OF_RESOURCE; goto exit; }

/* 使用非阻塞通信实现两个根进程之间的交换 */
err = MCA_PML_CALL(isend(sbuf, scount, sdtype, 0,
MCA_COLL_BASE_TAG_ALLGATHER,
MCA_PML_BASE_SEND_STANDARD,
comm, &reqs[rsize]));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

err = MCA_PML_CALL(irecv(rbuf, rcount, rdtype, 0,
MCA_COLL_BASE_TAG_ALLGATHER, comm,
&reqs[0]));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

/* 接收非根节点的信息 */
incr = rextent * rcount;
ptmp = (char *) rbuf + incr;
for (i = 1; i < rsize; ++i, ptmp += incr) {
err = MCA_PML_CALL(irecv(ptmp, rcount, rdtype, i,
MCA_COLL_BASE_TAG_ALLGATHER,
comm, &reqs[i]));
if (MPI_SUCCESS != err) { line = __LINE__; goto exit; }
}

// wait,直到这个request结束,也是用while做
err = ompi_request_wait_all(rsize + 1, reqs, MPI_STATUSES_IGNORE);
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

/* Step 2: exchange the resuts between the root processes */
span = opal_datatype_span(&sdtype->super, (int64_t)scount * (int64_t)size, &gap);
tmpbuf_free = (char *) malloc(span);
if (NULL == tmpbuf_free) { line = __LINE__; err = OMPI_ERR_OUT_OF_RESOURCE; goto exit; }
tmpbuf = tmpbuf_free - gap;

err = MCA_PML_CALL(isend(rbuf, rsize * rcount, rdtype, 0,
MCA_COLL_BASE_TAG_ALLGATHER,
MCA_PML_BASE_SEND_STANDARD, comm, &req));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

err = MCA_PML_CALL(recv(tmpbuf, size * scount, sdtype, 0,
MCA_COLL_BASE_TAG_ALLGATHER, comm,
MPI_STATUS_IGNORE));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

err = ompi_request_wait( &req, MPI_STATUS_IGNORE);
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }
}


/* Step 3: 广播数据到远程组。 这在两个组中同时发生,因此我们不能使用 coll_bcast(这会死锁)。
*/
if (rank != root) {
/* post the recv */
err = MCA_PML_CALL(recv(rbuf, rsize * rcount, rdtype, 0,
MCA_COLL_BASE_TAG_ALLGATHER, comm,
MPI_STATUS_IGNORE));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }

} else {
/* Send the data to every other process in the remote group except to rank zero. which has it already. */
for (i = 1; i < rsize; i++) {
err = MCA_PML_CALL(isend(tmpbuf, size * scount, sdtype, i,
MCA_COLL_BASE_TAG_ALLGATHER,
MCA_PML_BASE_SEND_STANDARD,
comm, &reqs[i - 1]));
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }
}

err = ompi_request_wait_all(rsize - 1, reqs, MPI_STATUSES_IGNORE);
if (OMPI_SUCCESS != err) { line = __LINE__; goto exit; }
}

exit:
if( MPI_SUCCESS != err ) {
OPAL_OUTPUT( (ompi_coll_base_framework.framework_output,"%s:%4d\tError occurred %d, rank %2d",
__FILE__, line, err, rank) );
(void)line; // silence compiler warning
if( NULL != reqs ) ompi_coll_base_free_reqs(reqs, rsize+1);
}
if (NULL != tmpbuf_free) {
free(tmpbuf_free);
}

return err;
}

以下是几种allgather算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
/*
allgather using O(log(N)) steps.

Bruck et al., "Efficient Algorithms for All-to-all Communications in Multiport Message-Passing Systems"
* Memory requirements: non-zero ranks require shift buffer to perform final
* step in the algorithm.
*
* Example on 6 nodes:
* Initialization: 每个进程在 rbuf 的位置 0 处都有自己的缓冲区。
* 这意味着如果用户为 sendbuf 指定了 MPI_IN_PLACE,
* 我们必须将我们的块从 recvbuf 复制到开始!
* # 0 1 2 3 4 5
* [0] [1] [2] [3] [4] [5]
* Step 0: 发给 (rank - 2^0), 从 (rank + 2^0) 接收
* # 0 1 2 3 4 5
* [0] [1] [2] [3] [4] [5]
* [1] [2] [3] [4] [5] [0]
* Step 1: 发给 (rank - 2^1), 从 (rank + 2^1) 接收
* 消息长度是从 0 到 2^1*block size,就是2倍的第一步
* # 0 1 2 3 4 5
* [0] [1] [2] [3] [4] [5]
* [1] [2] [3] [4] [5] [0]
* [2] [3] [4] [5] [0] [1]
* [3] [4] [5] [0] [1] [2]
* Step 2: 发给 (rank - 2^2), 从 (rank + 2^2) 接收
* 消息长度是剩下的所有块
* # 0 1 2 3 4 5
* [0] [1] [2] [3] [4] [5]
* [1] [2] [3] [4] [5] [0]
* [2] [3] [4] [5] [0] [1]
* [3] [4] [5] [0] [1] [2]
* [4] [5] [0] [1] [2] [3]
* [5] [0] [1] [2] [3] [4]
* Finalization: 进行本地转移以在正确的位置获取数据
* # 0 1 2 3 4 5
* [0] [0] [0] [0] [0] [0]
* [1] [1] [1] [1] [1] [1]
* [2] [2] [2] [2] [2] [2]
* [3] [3] [3] [3] [3] [3]
* [4] [4] [4] [4] [4] [4]
* [5] [5] [5] [5] [5] [5]
*/
int ompi_coll_base_allgather_intra_bruck(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void* rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int line = -1, rank, size, sendto, recvfrom, distance, blockcount, err = 0;
ptrdiff_t rlb, rext;
char *tmpsend = NULL, *tmprecv = NULL;

size = ompi_comm_size(comm);
rank = ompi_comm_rank(comm);

OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"coll:base:allgather_intra_bruck rank %d", rank));

err = ompi_datatype_get_extent (rdtype, &rlb, &rext);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Initialization step:
- if send buffer is not MPI_IN_PLACE, copy send buffer to block 0 of
receive buffer, else
- if rank r != 0, copy r^th block from receive buffer to block 0.
*/
tmprecv = (char*) rbuf;
if (MPI_IN_PLACE != sbuf) {
tmpsend = (char*) sbuf;
err = ompi_datatype_sndrcv(tmpsend, scount, sdtype, tmprecv, rcount, rdtype);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

} else if (0 != rank) { /* non root with MPI_IN_PLACE */
tmpsend = ((char*)rbuf) + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext;
err = ompi_datatype_copy_content_same_ddt(rdtype, rcount, tmprecv, tmpsend);
if (err < 0) { line = __LINE__; goto err_hndl; }
}

/* Communication step:
At every step i, rank r:
- doubles the distance
- sends message which starts at begining of rbuf and has size
(blockcount * rcount) to rank (r - distance)
- receives message of size blockcount * rcount from rank (r + distance)
at location (rbuf + distance * rcount * rext)
- blockcount doubles until last step when only the remaining data is
exchanged.
*/
blockcount = 1;
tmpsend = (char*) rbuf;
for (distance = 1; distance < size; distance<<=1) {

recvfrom = (rank + distance) % size;
sendto = (rank - distance + size) % size;

tmprecv = tmpsend + (ptrdiff_t)distance * (ptrdiff_t)rcount * rext;

if (distance <= (size >> 1)) {
blockcount = distance;
} else {
blockcount = size - distance;
}

/* Sendreceive
* 如果是同一进程的话就是直接拷贝,否则执行recv-send-wait
*/
err = ompi_coll_base_sendrecv(tmpsend, blockcount * rcount, rdtype,
sendto, MCA_COLL_BASE_TAG_ALLGATHER,
tmprecv, blockcount * rcount, rdtype,
recvfrom, MCA_COLL_BASE_TAG_ALLGATHER,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

}

/* Finalization step:
除了0号进程, 重排数据:
- 创建临时数组
- copy blocks [0 .. (size - rank - 1)] from rbuf to shift buffer
- move blocks [(size - rank) .. size] from rbuf to begining of rbuf
- copy blocks from shift buffer starting at block [rank] in rbuf.
*/
if (0 != rank) {
char *free_buf = NULL, *shift_buf = NULL;
ptrdiff_t span, gap = 0;

span = opal_datatype_span(&rdtype->super, (int64_t)(size - rank) * rcount, &gap);

free_buf = (char*)calloc(span, sizeof(char));
if (NULL == free_buf) {
line = __LINE__; err = OMPI_ERR_OUT_OF_RESOURCE; goto err_hndl;
}
shift_buf = free_buf - gap;

/* 1. copy blocks [0 .. (size - rank - 1)] from rbuf to shift buffer */
err = ompi_datatype_copy_content_same_ddt(rdtype, ((ptrdiff_t)(size - rank) * (ptrdiff_t)rcount),
shift_buf, rbuf);
if (err < 0) { line = __LINE__; free(free_buf); goto err_hndl; }

/* 2. move blocks [(size - rank) .. size] from rbuf to the begining of rbuf */
tmpsend = (char*) rbuf + (ptrdiff_t)(size - rank) * (ptrdiff_t)rcount * rext;
err = ompi_datatype_copy_content_same_ddt(rdtype, (ptrdiff_t)rank * (ptrdiff_t)rcount,
rbuf, tmpsend);
if (err < 0) { line = __LINE__; free(free_buf); goto err_hndl; }

/* 3. copy blocks from shift buffer back to rbuf starting at block [rank]. */
tmprecv = (char*) rbuf + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext;
err = ompi_datatype_copy_content_same_ddt(rdtype, (ptrdiff_t)(size - rank) * (ptrdiff_t)rcount,
tmprecv, shift_buf);
if (err < 0) { line = __LINE__; free(free_buf); goto err_hndl; }

free(free_buf);
}

return OMPI_SUCCESS;

err_hndl:
OPAL_OUTPUT((ompi_coll_base_framework.framework_output, "%s:%4d\tError occurred %d, rank %2d",
__FILE__, line, err, rank));
(void)line; // silence compiler warning
return err;
}

/*
allgather using O(log(N)) steps.

Recursive doubling algorithm for MPI_Allgather implementation. This algorithm is used in MPICH-2 for small- and medium-sized messages on power-of-two processes.

当前的实现仅适用于二次幂个进程。 如果在非二次幂进程上调用此算法,则将调用布鲁克算法。这是蝶形的方法

* Example on 4 nodes:
* Initialization: everyone has its own buffer at location rank in rbuf
* # 0 1 2 3
* [0] [ ] [ ] [ ]
* [ ] [1] [ ] [ ]
* [ ] [ ] [2] [ ]
* [ ] [ ] [ ] [3]
* Step 0: exchange data with (rank ^ 2^0)
* # 0 1 2 3
* [0] [0] [ ] [ ]
* [1] [1] [ ] [ ]
* [ ] [ ] [2] [2]
* [ ] [ ] [3] [3]
* Step 1: exchange data with (rank ^ 2^1) (if you can)
* # 0 1 2 3
* [0] [0] [0] [0]
* [1] [1] [1] [1]
* [2] [2] [2] [2]
* [3] [3] [3] [3]
*
* 我们可以修改代码以使用与 MPICH-2 相同的实现:
* - 使用递归减半算法,在每一步结束时,确定是否有节点在该步骤中没有交换数据,并向它们发送适当的消息。
*/
int
ompi_coll_base_allgather_intra_recursivedoubling(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void* rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int line = -1, rank, size, pow2size, err;
int remote, distance, sendblocklocation;
ptrdiff_t rlb, rext;
char *tmpsend = NULL, *tmprecv = NULL;

size = ompi_comm_size(comm);
rank = ompi_comm_rank(comm);

pow2size = opal_next_poweroftwo (size);
pow2size >>=1;

/* 当前的实现只处理进程的二次幂。 如果该函数在非二次幂的进程数上调用,
* 则打印警告并使用相同的参数调用 bruck allgather 算法。
*/
if (pow2size != size) {
OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"coll:base:allgather_intra_recursivedoubling WARNING: non-pow-2 size %d, switching to bruck algorithm",
size));

return ompi_coll_base_allgather_intra_bruck(sbuf, scount, sdtype,
rbuf, rcount, rdtype,
comm, module);
}

OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"coll:base:allgather_intra_recursivedoubling rank %d, size %d",
rank, size));

err = ompi_datatype_get_extent (rdtype, &rlb, &rext);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Initialization step:
- if send buffer is not MPI_IN_PLACE, copy send buffer to block 0 of
receive buffer
*/
if (MPI_IN_PLACE != sbuf) {
tmpsend = (char*) sbuf;
tmprecv = (char*) rbuf + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext;
err = ompi_datatype_sndrcv(tmpsend, scount, sdtype, tmprecv, rcount, rdtype);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

}

/* Communication step:
At every step i, rank r:
- exchanges message with rank remote = (r ^ 2^i).

*/
sendblocklocation = rank;
for (distance = 0x1; distance < size; distance<<=1) {
remote = rank ^ distance;

if (rank < remote) {
tmpsend = (char*)rbuf + (ptrdiff_t)sendblocklocation * (ptrdiff_t)rcount * rext;
tmprecv = (char*)rbuf + (ptrdiff_t)(sendblocklocation + distance) * (ptrdiff_t)rcount * rext;
} else {
tmpsend = (char*)rbuf + (ptrdiff_t)sendblocklocation * (ptrdiff_t)rcount * rext;
tmprecv = (char*)rbuf + (ptrdiff_t)(sendblocklocation - distance) * (ptrdiff_t)rcount * rext;
sendblocklocation -= distance;
}

/* Sendreceive */
err = ompi_coll_base_sendrecv(tmpsend, (ptrdiff_t)distance * (ptrdiff_t)rcount, rdtype,
remote, MCA_COLL_BASE_TAG_ALLGATHER,
tmprecv, (ptrdiff_t)distance * (ptrdiff_t)rcount, rdtype,
remote, MCA_COLL_BASE_TAG_ALLGATHER,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

}

return OMPI_SUCCESS;
}

/*
allgather using O(log(N)) steps.

* Description: 一种类似于 Bruck 的 allgather 算法的提议,但具有倒置的距离和不递减的交换数据大小.
* Described in "Sparbit: a new logarithmic-cost and data locality-aware MPI Allgather algorithm".

* Example on 6 nodes, with l representing the highest power of two smaller than N, in this case l =
* 4 (more details can be found on the paper):
* Initial state
* # 0 1 2 3 4 5
* [0] [ ] [ ] [ ] [ ] [ ]
* [ ] [1] [ ] [ ] [ ] [ ]
* [ ] [ ] [2] [ ] [ ] [ ]
* [ ] [ ] [ ] [3] [ ] [ ]
* [ ] [ ] [ ] [ ] [4] [ ]
* [ ] [ ] [ ] [ ] [ ] [5]
* Step 0: 每个进程将自己的块发送到进程 r + l 并从 r - l 接收另一个块。
* # 0 1 2 3 4 5
* [0] [ ] [ ] [ ] [0] [ ]
* [ ] [1] [ ] [ ] [ ] [1]
* [2] [ ] [2] [ ] [ ] [ ]
* [ ] [3] [ ] [3] [ ] [ ]
* [ ] [ ] [4] [ ] [4] [ ]
* [ ] [ ] [ ] [5] [ ] [5]
* Step 1: 每个进程将自己的块发送到进程 r + l/2 并从 r - l/2 接收另一个块。
* 上一步接收到的块被忽略以避免未来的双重写入。
* # 0 1 2 3 4 5
* [0] [ ] [0] [ ] [0] [ ]
* [ ] [1] [ ] [1] [ ] [1]
* [2] [ ] [2] [ ] [2] [ ]
* [ ] [3] [ ] [3] [ ] [3]
* [4] [ ] [4] [ ] [4] [ ]
* [ ] [5] [ ] [5] [ ] [5]
* Step 1: 每个进程将其拥有的所有数据(3 个块)发送到进程 r + l/4,
* 并类似地从进程 r - l/4 接收所有数据。
* # 0 1 2 3 4 5
* [0] [0] [0] [0] [0] [0]
* [1] [1] [1] [1] [1] [1]
* [2] [2] [2] [2] [2] [2]
* [3] [3] [3] [3] [3] [3]
* [4] [4] [4] [4] [4] [4]
* [5] [5] [5] [5] [5] [5]
*/

int ompi_coll_base_allgather_intra_sparbit(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void* rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
/* list of variable declaration */
int rank = 0, comm_size = 0, comm_log = 0, exclusion = 0, data_expected = 1, transfer_count = 0;
int sendto, recvfrom, send_disp, recv_disp;
uint32_t last_ignore, ignore_steps, distance = 1;

int err = 0;
int line = -1;

ptrdiff_t rlb, rext;

char *tmpsend = NULL, *tmprecv = NULL;

MPI_Request *requests = NULL;

comm_size = ompi_comm_size(comm);
rank = ompi_comm_rank(comm);

err = ompi_datatype_get_extent(rdtype, &rlb, &rext);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* 如果未设置 MPI_IN_PLACE 条件,则将发送缓冲区复制到接收缓冲区以执行发送(所有数据都从 recv 缓冲区中提取和转发)
/* tmprecv 和 tmpsend 用作抽象指针以简化发送和接收缓冲区的选择
*/
tmprecv = (char *) rbuf;
if(MPI_IN_PLACE != sbuf){
tmpsend = (char *) sbuf;
err = ompi_datatype_sndrcv(tmpsend, scount, sdtype, tmprecv + (ptrdiff_t) rank * rcount * rext, rcount, rdtype);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}
tmpsend = tmprecv;

requests = (MPI_Request *) malloc(comm_size * sizeof(MPI_Request));

/* calculate log2 of the total process count */
comm_log = ceil(log(comm_size)/log(2));
distance <<= comm_log - 1;

last_ignore = __builtin_ctz(comm_size);
ignore_steps = (~((uint32_t) comm_size >> last_ignore) | 1) << last_ignore;

/* perform the parallel binomial tree distribution steps */
for (int i = 0; i < comm_log; ++i) {
sendto = (rank + distance) % comm_size;
recvfrom = (rank - distance + comm_size) % comm_size;
exclusion = (distance & ignore_steps) == distance;

for (transfer_count = 0; transfer_count < data_expected - exclusion; transfer_count++) {
send_disp = (rank - 2 * transfer_count * distance + comm_size) % comm_size;
recv_disp = (rank - (2 * transfer_count + 1) * distance + comm_size) % comm_size;

/* 由于每个进程发送几个不连续的数据块,因此发送的每个块(因此每个发送和接收调用)都需要不同的标签。 */
/* 由于基本 OpenMPI 只为 allgather 提供一个标签,我们被迫在 send 和 recv 调用中使用来自其他组件的标签空间 */
MCA_PML_CALL(isend(tmpsend + (ptrdiff_t) send_disp * scount * rext, scount, rdtype, sendto, MCA_COLL_BASE_TAG_HCOLL_BASE - send_disp, MCA_PML_BASE_SEND_STANDARD, comm, requests + transfer_count));
MCA_PML_CALL(irecv(tmprecv + (ptrdiff_t) recv_disp * rcount * rext, rcount, rdtype, recvfrom, MCA_COLL_BASE_TAG_HCOLL_BASE - recv_disp, comm, requests + data_expected - exclusion + transfer_count));
}
ompi_request_wait_all(transfer_count * 2, requests, MPI_STATUSES_IGNORE);

distance >>= 1;
/* calculates the data expected for the next step, based on the current number of blocks and eventual exclusions */
data_expected = (data_expected << 1) - exclusion;
exclusion = 0;
}

free(requests);

return OMPI_SUCCESS;

err_hndl:
OPAL_OUTPUT((ompi_coll_base_framework.framework_output, "%s:%4d\tError occurred %d, rank %2d",
__FILE__, line, err, rank));
(void)line; // silence compiler warning
return err;
}

/*
allgather using O(N) steps.
allgather的环形算法。 在i步 ,rank r 接收来自 rank (r - 1) 的消息,其中包含来自 rank (r - i - 1) 的数据,并将包含来自 rank (r - i) 的数据的消息发送到 rank (r + 1)
*/
int ompi_coll_base_allgather_intra_ring(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void* rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int line = -1, rank, size, err, sendto, recvfrom, i, recvdatafrom, senddatafrom;
ptrdiff_t rlb, rext;
char *tmpsend = NULL, *tmprecv = NULL;

size = ompi_comm_size(comm);
rank = ompi_comm_rank(comm);

OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"coll:base:allgather_intra_ring rank %d", rank));

err = ompi_datatype_get_extent (rdtype, &rlb, &rext);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Initialization step:
- if send buffer is not MPI_IN_PLACE, copy send buffer to appropriate block
of receive buffer
*/
tmprecv = (char*) rbuf + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext;
if (MPI_IN_PLACE != sbuf) {
tmpsend = (char*) sbuf;
err = ompi_datatype_sndrcv(tmpsend, scount, sdtype, tmprecv, rcount, rdtype);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}

/* Communication step:
At every step i: 0 .. (P-1), rank r:
- 从 [(r - 1 + size) % size] 接收数据,其中包含 [(r - i - 1 + size) % size] 的数据
- 发送给下一个进程[(r + 1) % size],其中包含 [(r - i + size) % size]的数据
*/
sendto = (rank + 1) % size;
recvfrom = (rank - 1 + size) % size;

for (i = 0; i < size - 1; i++) {
recvdatafrom = (rank - i - 1 + size) % size;
senddatafrom = (rank - i + size) % size;

tmprecv = (char*)rbuf + (ptrdiff_t)recvdatafrom * (ptrdiff_t)rcount * rext;
tmpsend = (char*)rbuf + (ptrdiff_t)senddatafrom * (ptrdiff_t)rcount * rext;

/* Sendreceive */
err = ompi_coll_base_sendrecv(tmpsend, rcount, rdtype, sendto,
MCA_COLL_BASE_TAG_ALLGATHER,
tmprecv, rcount, rdtype, recvfrom,
MCA_COLL_BASE_TAG_ALLGATHER,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

}

return OMPI_SUCCESS;
}

/*
allgather using N/2 steps (O(N))

Neighbor Exchange algorithm for allgather. Described by Chen et.al. in
"Performance Evaluation of Allgather Algorithms on Terascale Linux Cluster with Fast Ethernet",

Rank r 与其中一个邻居交换消息,并在下一步进一步转发数据。算法仅适用于偶数进程。 对于奇数个进程,我们切换到环形算法。

Example on 6 nodes:
Initial state
# 0 1 2 3 4 5
[0] [ ] [ ] [ ] [ ] [ ]
[ ] [1] [ ] [ ] [ ] [ ]
[ ] [ ] [2] [ ] [ ] [ ]
[ ] [ ] [ ] [3] [ ] [ ]
[ ] [ ] [ ] [ ] [4] [ ]
[ ] [ ] [ ] [ ] [ ] [5]
Step 0:
# 0 1 2 3 4 5
[0] [0] [ ] [ ] [ ] [ ]
[1] [1] [ ] [ ] [ ] [ ]
[ ] [ ] [2] [2] [ ] [ ]
[ ] [ ] [3] [3] [ ] [ ]
[ ] [ ] [ ] [ ] [4] [4]
[ ] [ ] [ ] [ ] [5] [5]
Step 1:
# 0 1 2 3 4 5
[0] [0] [0] [ ] [ ] [0]
[1] [1] [1] [ ] [ ] [1]
[ ] [2] [2] [2] [2] [ ]
[ ] [3] [3] [3] [3] [ ]
[4] [ ] [ ] [4] [4] [4]
[5] [ ] [ ] [5] [5] [5]
Step 2:
# 0 1 2 3 4 5
[0] [0] [0] [0] [0] [0]
[1] [1] [1] [1] [1] [1]
[2] [2] [2] [2] [2] [2]
[3] [3] [3] [3] [3] [3]
[4] [4] [4] [4] [4] [4]
[5] [5] [5] [5] [5] [5]
*/
int
ompi_coll_base_allgather_intra_neighborexchange(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void* rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int line = -1, rank, size, i, even_rank, err;
int neighbor[2], offset_at_step[2], recv_data_from[2], send_data_from;
ptrdiff_t rlb, rext;
char *tmpsend = NULL, *tmprecv = NULL;

size = ompi_comm_size(comm);
rank = ompi_comm_rank(comm);

if (size % 2) {
OPAL_OUTPUT((ompi_coll_base_framework.framework_output, "coll:base:allgather_intra_neighborexchange WARNING: odd size %d, switching to ring algorithm", size));
return ompi_coll_base_allgather_intra_ring(sbuf, scount, sdtype, rbuf, rcount, rdtype, comm, module);
}

err = ompi_datatype_get_extent (rdtype, &rlb, &rext);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Initialization step:
- if send buffer is not MPI_IN_PLACE, copy send buffer to appropriate block
of receive buffer
*/
tmprecv = (char*) rbuf + (ptrdiff_t)rank *(ptrdiff_t) rcount * rext;
if (MPI_IN_PLACE != sbuf) {
tmpsend = (char*) sbuf;
err = ompi_datatype_sndrcv(tmpsend, scount, sdtype, tmprecv, rcount, rdtype);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}

/* Determine neighbors, order in which blocks will arrive, etc. */
even_rank = !(rank % 2);
if (even_rank) {
neighbor[0] = (rank + 1) % size;
neighbor[1] = (rank - 1 + size) % size;
recv_data_from[0] = rank;
recv_data_from[1] = rank;
offset_at_step[0] = (+2);
offset_at_step[1] = (-2);
} else {
neighbor[0] = (rank - 1 + size) % size;
neighbor[1] = (rank + 1) % size;
recv_data_from[0] = neighbor[0];
recv_data_from[1] = neighbor[0];
offset_at_step[0] = (-2);
offset_at_step[1] = (+2);
}

/* Communication loop:
- First step is special: exchange a single block with neighbor[0].
- Rest of the steps:
根据偏移量更新recv_data_from,以及
与适当的邻居交换两个块。
发送位置成为先前的接收位置。
*/
tmprecv = (char*)rbuf + (ptrdiff_t)neighbor[0] * (ptrdiff_t)rcount * rext;
tmpsend = (char*)rbuf + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext;
/* Sendreceive */
err = ompi_coll_base_sendrecv(tmpsend, rcount, rdtype, neighbor[0],
MCA_COLL_BASE_TAG_ALLGATHER,
tmprecv, rcount, rdtype, neighbor[0],
MCA_COLL_BASE_TAG_ALLGATHER,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Determine initial sending location */
if (even_rank) {
send_data_from = rank;
} else {
send_data_from = recv_data_from[0];
}

for (i = 1; i < (size / 2); i++) {
const int i_parity = i % 2;
recv_data_from[i_parity] =
(recv_data_from[i_parity] + offset_at_step[i_parity] + size) % size;

tmprecv = (char*)rbuf + (ptrdiff_t)recv_data_from[i_parity] * (ptrdiff_t)rcount * rext;
tmpsend = (char*)rbuf + (ptrdiff_t)send_data_from * rcount * rext;

/* Sendreceive */
err = ompi_coll_base_sendrecv(tmpsend, (ptrdiff_t)2 * (ptrdiff_t)rcount, rdtype,
neighbor[i_parity],
MCA_COLL_BASE_TAG_ALLGATHER,
tmprecv, (ptrdiff_t)2 * (ptrdiff_t)rcount, rdtype,
neighbor[i_parity],
MCA_COLL_BASE_TAG_ALLGATHER,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

send_data_from = recv_data_from[i_parity];
}

return OMPI_SUCCESS;
}


int ompi_coll_base_allgather_intra_two_procs(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void* rbuf, int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int line = -1, err, rank, remote;
char *tmpsend = NULL, *tmprecv = NULL;
ptrdiff_t rext, lb;

rank = ompi_comm_rank(comm);

OPAL_OUTPUT((ompi_coll_base_framework.framework_output,
"ompi_coll_base_allgather_intra_two_procs rank %d", rank));

if (2 != ompi_comm_size(comm)) {
return MPI_ERR_UNSUPPORTED_OPERATION;
}

err = ompi_datatype_get_extent (rdtype, &lb, &rext);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Exchange data:
- compute source and destinations
- send receive data
*/
remote = rank ^ 0x1;

tmpsend = (char*)sbuf;
if (MPI_IN_PLACE == sbuf) {
tmpsend = (char*)rbuf + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext;
scount = rcount;
sdtype = rdtype;
}
tmprecv = (char*)rbuf + (ptrdiff_t)remote * (ptrdiff_t)rcount * rext;

err = ompi_coll_base_sendrecv(tmpsend, scount, sdtype, remote,
MCA_COLL_BASE_TAG_ALLGATHER,
tmprecv, rcount, rdtype, remote,
MCA_COLL_BASE_TAG_ALLGATHER,
comm, MPI_STATUS_IGNORE, rank);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }

/* Place your data in correct location if necessary */
if (MPI_IN_PLACE != sbuf) {
err = ompi_datatype_sndrcv((char*)sbuf, scount, sdtype,
(char*)rbuf + (ptrdiff_t)rank * (ptrdiff_t)rcount * rext, rcount, rdtype);
if (MPI_SUCCESS != err) { line = __LINE__; goto err_hndl; }
}

return MPI_SUCCESS;
}


/* 线性函数是从 BASIC coll 模块复制的,它们不会对消息进行分段并且是简单的实现,
* 但对于一些少量节点和/或小数据大小,它们与基于基/树的分段操作一样快
*
* Function: - allgather using other MPI collections
* Accepts: - same as MPI_Allgather()
* Returns: - MPI_SUCCESS or error code
*/
int
ompi_coll_base_allgather_intra_basic_linear(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void *rbuf,
int rcount,
struct ompi_datatype_t *rdtype,
struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int err;
ptrdiff_t lb, extent;

/* Handle MPI_IN_PLACE -- note that rank 0 can use IN_PLACE
natively, and we can just alias the right position in rbuf
as sbuf and avoid using a temporary buffer if gather is
implemented correctly */
if (MPI_IN_PLACE == sbuf && 0 != ompi_comm_rank(comm)) {
ompi_datatype_get_extent(rdtype, &lb, &extent);
sbuf = ((char*) rbuf) + (ompi_comm_rank(comm) * extent * rcount);
sdtype = rdtype;
scount = rcount;
}

/* Gather and broadcast. */

err = comm->c_coll->coll_gather(sbuf, scount, sdtype,
rbuf, rcount, rdtype,
0, comm, comm->c_coll->coll_gather_module);
if (MPI_SUCCESS == err) {
size_t length = (ptrdiff_t)rcount * ompi_comm_size(comm);
if( length < (size_t)INT_MAX ) {
err = comm->c_coll->coll_bcast(rbuf, (ptrdiff_t)rcount * ompi_comm_size(comm), rdtype,
0, comm, comm->c_coll->coll_bcast_module);
} else {
ompi_datatype_t* temptype;
ompi_datatype_create_contiguous(ompi_comm_size(comm), rdtype, &temptype);
ompi_datatype_commit(&temptype);
err = comm->c_coll->coll_bcast(rbuf, rcount, temptype,
0, comm, comm->c_coll->coll_bcast_module);
ompi_datatype_destroy(&temptype);
}
}
return err;
}

MPI_Gather

首先检查缓冲区是否正确,通信域是否正确,是否跨通信域,如果没问题直接调用coll_gather,有如下几个实现:

1
2
3
mca_coll_basic_gather_inter
ompi_coll_base_gather_intra_basic_linear
mca_coll_self_gather_intra

以下几个实现很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int
mca_coll_basic_gather_inter(const void *sbuf, int scount,
struct ompi_datatype_t *sdtype,
void *rbuf, int rcount,
struct ompi_datatype_t *rdtype,
int root, struct ompi_communicator_t *comm,
mca_coll_base_module_t *module)
{
int i;
int err;
int size;
char *ptmp;
MPI_Aint incr;
MPI_Aint extent;
MPI_Aint lb;

size = ompi_comm_remote_size(comm);

if (MPI_PROC_NULL == root) {
/* do nothing */
err = OMPI_SUCCESS;
} else if (MPI_ROOT != root) {
/* Everyone but root sends data and returns. */
err = MCA_PML_CALL(send(sbuf, scount, sdtype, root,
MCA_COLL_BASE_TAG_GATHER,
MCA_PML_BASE_SEND_STANDARD, comm));
} else {
/* I am the root, loop receiving the data. */
err = ompi_datatype_get_extent(rdtype, &lb, &extent);
if (OMPI_SUCCESS != err) {
return OMPI_ERROR;
}

incr = extent * rcount;
for (i = 0, ptmp = (char *) rbuf; i < size; ++i, ptmp += incr) {
err = MCA_PML_CALL(recv(ptmp, rcount, rdtype, i,
MCA_COLL_BASE_TAG_GATHER,
comm, MPI_STATUS_IGNORE));
if (MPI_SUCCESS != err) {
return err;
}
}
}
return err;
}

Request结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

/**
* Main top-level request struct definition
*/
struct ompi_request_t {
opal_free_list_item_t super; /**< Base type */
ompi_request_type_t req_type; /**< Enum indicating the type of the request */
ompi_status_public_t req_status; /**< Completion status */
volatile void *req_complete; /**< Flag indicating wether request has completed */
volatile ompi_request_state_t req_state; /**< enum indicate state of the request */
bool req_persistent; /**< flag indicating if the this is a persistent request */
int req_f_to_c_index; /**< Index in Fortran <-> C translation array */
ompi_request_start_fn_t req_start; /**< Called by MPI_START and MPI_STARTALL */
ompi_request_free_fn_t req_free; /**< Called by free */
ompi_request_cancel_fn_t req_cancel; /**< Optional function to cancel the request */
ompi_request_complete_fn_t req_complete_cb; /**< Called when the request is MPI completed */
void *req_complete_cb_data;
ompi_mpi_object_t req_mpi_object; /**< Pointer to MPI object that created this request */
};

struct ompi_predefined_request_t {
struct ompi_request_t request;
char padding[PREDEFINED_REQUEST_PAD - sizeof(ompi_request_t)];
};

typedef struct ompi_predefined_request_t ompi_predefined_request_t;

/**
* 初始化一个请求。 这是一个避免函数调用开销的宏,因为它通常在关键性能路径中调用(因为请求可能被重用,我们可能必须多次初始化请求)。
*/
#define OMPI_REQUEST_INIT(request, persistent) \
do { \
(request)->req_complete = \
(persistent) ? REQUEST_COMPLETED : REQUEST_PENDING; \
(request)->req_state = OMPI_REQUEST_INACTIVE; \
(request)->req_persistent = (persistent); \
(request)->req_complete_cb = NULL; \
(request)->req_complete_cb_data = NULL; \
} while (0);


#define REQUEST_COMPLETE(req) (REQUEST_COMPLETED == (req)->req_complete)
/**
* 完成请求。 这是一个避免函数调用开销的宏,因为它通常在关键性能路径中调用(因为请求可能被重用,我们可能不得不多次完成一个请求)。
* 当最终确定一个请求时,如果之前对该请求调用了 MPI_Request_f2c(),则该请求已添加到 f2c 表中,我们需要将其删除
* 该函数只能从 MPI 层调用。 永远不要从 PML 调用它。
* 它负责上层清理工作。 当用户调用 MPI_Request_free 时,我们应该释放所有 MPI 级别的资源,所以我们也必须调用这个函数。
*/
#define OMPI_REQUEST_FINI(request) \
do { \
(request)->req_state = OMPI_REQUEST_INVALID; \
if (MPI_UNDEFINED != (request)->req_f_to_c_index) { \
opal_pointer_array_set_item(&ompi_request_f_to_c_table, \
(request)->req_f_to_c_index, NULL); \
(request)->req_f_to_c_index = MPI_UNDEFINED; \
} \
} while (0);

/*
* 除了在返回 MPI_ERR_IN_STATUS 的过程中,状态对象的 MPI_ERROR 字段永远不会被修改
*/
#define OMPI_COPY_STATUS(pdst, src, is_err_in_status) \
do { \
if (is_err_in_status) { \
*(pdst) = (src); \
} \
else { \
(pdst)->MPI_TAG = (src).MPI_TAG; \
(pdst)->MPI_SOURCE = (src).MPI_SOURCE; \
(pdst)->_ucount = (src)._ucount; \
(pdst)->_cancelled = (src)._cancelled; \
} \
} while(0);

/**
* request相关的函数
*/
typedef struct ompi_request_fns_t {
ompi_request_test_fn_t req_test;
ompi_request_test_any_fn_t req_test_any;
ompi_request_test_all_fn_t req_test_all;
ompi_request_test_some_fn_t req_test_some;
ompi_request_wait_fn_t req_wait;
ompi_request_wait_any_fn_t req_wait_any;
ompi_request_wait_all_fn_t req_wait_all;
ompi_request_wait_some_fn_t req_wait_some;
} ompi_request_fns_t;

c++11 最基础最简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#pragma once
#include<vector>
#include<queue>
#include<memory>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
#include<functional>
#include<stdexcept>
#include<type_traits>

class ThreadPool
{
public:
ThreadPool();
ThreadPool(int num);
~ThreadPool();

template<class F, class... Args>
std::future<int> enqueue(F& f, Args&... args);

private:
std::vector <std::thread> workers; //thread array

std::queue<std::function<void()>>tasks; //task queue

std::mutex queue_mutex;
std::condition_variable cond;
bool stop;
};

inline ThreadPool::ThreadPool()
{
}

ThreadPool::ThreadPool(int num) :stop(false)
{
for (size_t i = 0; i < num; i++)
{
auto thread = [this] {
for (;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->cond.wait(lock, [this] {
return this->stop || !this->tasks.empty();//stop 或许任务队列不为空时唤醒。
}
);

if (stop && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();//有点类似bfs的思路
}

task();
}
};
workers.emplace_back(thread);

}
}

inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
cond.notify_all();

for (auto& worker : workers)
{
worker.join();
}
}


//}

template<class F, class... Args>
std::future<int> ThreadPool::enqueue(F& f, Args&... args)
{
std::function<decltype(f(args...))()> func =
std::bind(std::forward<F>(f), std::forward<Args>(args)...); // 连接函数和参数定义,特殊函数类型,避免左右值错误

auto task = std::make_shared< std::packaged_task<int()> >(
func
);

std::function<void()> warpper_func = [task](){(*task)();};
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(warpper_func);
cond.notify_one();
return task->get_future();
}

test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main()
{
ThreadPool pool(4);
std::vector<std::future<int>>results;

for (size_t i = 0; i < 8; i++)
{
auto tp = [i] {
std::cout << "hello " << i << std::endl;
// std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "world " << i << std::endl;
return i * i;

};
results.emplace_back(
pool.enqueue((tp))
);

}



for (auto&& result : results)
std::cout << result.get() << ' ';
std::cout << std::endl;

return 0;
}

C++11/14 一般性的线程池

上面我们有很多不足,比如限定了task function的返回值,而且逻辑不够优美。我们接着进行优化!

关于task function的 return type 是未定的问题,c++11 给出了两种方式。一个是decltype(expr),另外一个是std::result_of. 都可以通过尾置返回值类型进行处理。

我们首先使用decltype. 完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#pragma once
#include<vector>
#include<queue>
#include<memory>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
#include<functional>
#include<stdexcept>
#include<type_traits>

class ThreadPool
{
public:
ThreadPool();
ThreadPool(int num);
~ThreadPool();
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
// ->std::future<decltype(f(args...))>;
private:
std::vector <std::thread> workers; //thread array

std::queue<std::function<void()>>tasks; //task queue

std::mutex queue_mutex;
std::condition_variable cond;
bool stop;
};

inline ThreadPool::ThreadPool()
{
}

ThreadPool::ThreadPool(int num) :stop(false)
{
for (size_t i = 0; i < num; i++)
{
auto thread = [this] {
for (;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->cond.wait(lock, [this] {
return this->stop || !this->tasks.empty();//stop 或许任务队列不为空时唤醒。
}
);

if (stop && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();//有点类似bfs的思路
}

task();
}
};
workers.emplace_back(thread);

}
}

inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
cond.notify_all();

for (auto& worker : workers)
{
worker.join();
}
}


template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<decltype(f(args...))>
{
using ret_type = std::future<decltype(f(args...))>;
std::function<decltype(f(args...))()> func =
std::bind(std::forward<F>(f), std::forward<Args>(args)...); // 连接函数和参数定义,特殊函数类型,避免左右值错误

auto task = std::make_shared< std::packaged_task<decltype(f(args...))()> >(
func
);

std::function<void()> warpper_func = [task]() {(*task)(); };
ret_type res = task->get_future();
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(warpper_func);
cond.notify_one();
return res;
}

优化版createthread 单独抽出来组成一个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#pragma once
#include<vector>
#include<queue>
#include<memory>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
#include<functional>
#include<stdexcept>
#include<type_traits>
#include <utility>

class ThreadPool
{
public:
ThreadPool();
ThreadPool(int num);
~ThreadPool();

void CreateThread(void);

//template<class F, class...Args>
//auto enqueue(F&& f, Args&&...args)->std::future<typename std::_Forced_result_type<F(Args...)>::type>;
template<class F, class... Args>
auto enqueue(F &&f, Args&&... args)
->std::future<decltype(f(args...))>;
private:
std::vector <std::thread> workers; //thread array

std::queue<std::function<void()>>tasks; //task queue

std::mutex queue_mutex;
std::condition_variable cond;
bool stop;
};

inline ThreadPool::ThreadPool()
{
}

inline void ThreadPool::CreateThread(void)
{
for (;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->cond.wait(lock, [this] {
return this->stop || !this->tasks.empty();//stop 或许任务队列不为空时唤醒。
}
);

if (stop && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();//有点类似bfs的思路
}

task();
}
}

ThreadPool::ThreadPool(int num) :stop(false)
{
for (size_t i = 0; i < num; i++)
{
auto thread = std::bind(&ThreadPool::CreateThread,this);//&,this 不能丢
workers.emplace_back(thread);

//或者直接下面
//workers.emplace_back(std::bind(&ThreadPool::CreateThread, this));

}
}

inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
cond.notify_all();

for (auto& worker : workers)
{
worker.join();
}
}


template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
->std::future<decltype(f(args...))>
{
using ret_type = std::future< decltype(f(args...))>; //typename 此处加不加均可以的,下面同
std::function< decltype(f(args...))()> func =
std::bind(std::forward<F>(f), std::forward<Args>(args)...); // 连接函数和参数定义,特殊函数类型,避免左右值错误

auto task = std::make_shared< std::packaged_task< decltype(f(args...))()> >(
func
);

std::function<void()> warpper_func = [task]() {(*task)(); };
ret_type res = task->get_future();
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(warpper_func);
cond.notify_one();
return res;
}

test 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main()
{
ThreadPool pool(4);
std::vector<std::future<int>>results;

for (int i = 0; i < 8; i++)
{
auto tp = [i] {
std::cout << "hello " << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "world " << i << std::endl;
return i * i;

};
auto ans = pool.enqueue(std::move(tp));
results.emplace_back(std::move(ans));

}


for (auto&& result : results)
std::cout << result.get() << ' ';
std::cout << std::endl;

return 0;
}

那我们现在采用第二种方式。

需要更改的地方如下:

enqueue 的声明:

1
2
3
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
->std::future<typename std::result_of<F(Args...)>::type>;

enqueue 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
->std::future<typename std::result_of<F(Args...)>::type>
{
using ret_type = std::future<typename std::result_of<F(Args...)>::type>; //typename 此处加不加均可以的,下面同
std::function<typename std::result_of<F(Args...)>::type()> func =
std::bind(std::forward<F>(f), std::forward<Args>(args)...); // 连接函数和参数定义,特殊函数类型,避免左右值错误

auto task = std::make_shared< std::packaged_task<typename std::result_of<F(Args...)>::type()> >(
func
);

std::function<void()> warpper_func = [task]() {(*task)(); };
ret_type res = task->get_future();
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(warpper_func);
cond.notify_one();
return res;
}

Leetcode1799. Maximize Score After N Operations

You are given nums, an array of positive integers of size 2 * n. You must perform n operations on this array.

In the ith operation (1-indexed), you will:

  • Choose two elements, x and y.
  • Receive a score of i * gcd(x, y).
  • Remove x and y from nums.

Return the maximum score you can receive after performing n operations.

The function gcd(x, y) is the greatest common divisor of x and y.

Example 1:

1
2
3
4
Input: nums = [1,2]
Output: 1
Explanation: The optimal choice of operations is:
(1 * gcd(1, 2)) = 1

Example 2:

1
2
3
4
Input: nums = [3,4,6,8]
Output: 11
Explanation: The optimal choice of operations is:
(1 * gcd(3, 6)) + (2 * gcd(4, 8)) = 3 + 8 = 11

Example 3:

1
2
3
4
Input: nums = [1,2,3,4,5,6]
Output: 14
Explanation: The optimal choice of operations is:
(1 * gcd(1, 5)) + (2 * gcd(2, 4)) + (3 * gcd(3, 6)) = 1 + 4 + 9 = 14

状态压缩dp,代码很好懂
``C++
class Solution {
public:

vector<int> num;
vector<int> f;
vector<vector<int>> gcd;
int n, maxstate;

int dfs(int state, int t) {
    if (f[state] != -1)
        return f[state];
    f[state] = 0;
    for (int i = 0; i < n; i ++)
        for (int j = i+1; j < n; j ++) {
            if (!(state & (1 << i)) || !(state & (1 << j)))
                continue;
            int v = t * gcd[i][j];
            int newstate = state - (1 << i) - (1 << j);
            f[state] = max(f[state], dfs(newstate, t+1) + v);
        }
    return f[state];
}

int maxScore(vector<int>& nums) {
    this->num = nums;
    n = nums.size();
    maxstate = 1 << n;

    f.assign(maxstate, -1);

    // 预处理gcd
    gcd.assign(n, vector<int>(n));
    for (int i = 0; i < n; i ++)
        for (int j = i+1; j < n; j ++)
            gcd[i][j] = __gcd(nums[i], nums[j]);

    return dfs(maxstate-1, 1);
}

};


也可以用bfs的方法,把状态变成一个图
```C++
class Solution {
public:

    vector<int> num;
    vector<int> f;
    vector<vector<int>> gcd;
    int n, maxstate;

    int bfs() {
        using pii = pair<int, int>;
        queue<pii> q;
        vector<bool> vis(maxstate, false);
        q.push({0, 1});
        f[0] = 0;

        while(!q.empty()) {
            printf("aaa %d\n", maxstate);
            pii p = q.front();
            q.pop();
            int state = p.first;
            int t = p.second;
            if (vis[state])
                continue;
            vis[state] = true;
            for (int i = 0; i < n; i ++)
                for (int j = i+1; j < n; j ++) {
                    if ((state & (1 << i)) || (state & (1 << j)))
                        continue;
                    int newstate = state | (1 << i) | (1 << j);
                    int score = t * gcd[i][j];
                    if (f[state] + score > f[newstate]) {
                        f[newstate] = f[state] + score;
                        q.push({newstate, t+1});
                    }
                }
        }
        return f[maxstate-1];
    }

    int maxScore(vector<int>& nums) {
        this->num = nums;
        n = nums.size();
        maxstate = 1 << n;

        f.assign(maxstate, -1);

        // 预处理gcd
        gcd.assign(n, vector<int>(n));
        for (int i = 0; i < n; i ++)
            for (int j = i+1; j < n; j ++)
                gcd[i][j] = __gcd(nums[i], nums[j]);

        return bfs();
    }
};

SSE技术简介

SIMD(single-instruction, multiple-data)是一种使用单道指令处理多道数据流的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。

从 SIMD 架构介绍可知,相较于 SISD架构,SIMD架构的计算机具有更高的理论峰值浮点算力,因而更适合计算密集型任务。如下图所示,以加法指令为例,在SISD架构计算机上,CPU先执行一条指令,进行A1 + B1 = C1计算,再执行下一条指令,进行A2 + B2 = C2计算,按此顺序依次完成后续计算。四个加法计算需依次串行执行四次。而对于SIMD指令来说,CPU只需执行一条指令,即可完成四个加法计算操作,四个加法计算操作并行执行。

img

图2 SISD 和 SIMD

SIMD 架构的计算机之所以能够并行化执行四个浮点数(甚至更多)操作的原因是支持 SIMD 指令的 CPU在设计时增加了一些专用的向量寄存器。SIMD向量寄存器的长度往往大于通用寄存器,比如SEE 的 XMM寄存器的长度为128位,AVX和AVX2的YMM寄存器为256位。因此,这些专用的向量寄存器可以同时放入多个数据。但需要注意,这里放入的多个数据需要保证数据类型是一致的。

Intel x86-64 SIMD 指令集

img

图3 Intel SIMD 指令集发展

  1. MMX 指令集),MMX(Multi Media eXtension,多媒体扩展指令集)指令集是Intel公司于1996年推出的一项多媒体指令增强技术。MMX指令集中包括有57条多媒体指令,通过这些指令可以一次处理多个数据,在处理结果超过实际处理能力的时候也能进行正常处理,这样在软件的配合下,就可以得到更高的性能。
  2. SSE/SSE2/SSE3/SSE4/SSE5 指令集,Intel在1999年推出SSE(Streaming SIMD eXtensions)指令集,是x86上对SIMD指令集的一个扩展,主要用于处理单精度浮点数。Intel陆续推出SSE2、SSE3、SSE4版本。其中,SSE主要处理单精度浮点数,SSE2引入了整数的处理,SSE指令集引入了8个128bit的寄存器,称为XMM0到XMM7,正因为这些寄存器存储了多个数据,使用一条指令处理,因此称这项功能为SIMD。
  3. AVX指令集,AVX在2008年3月提出,并在2011年 Sandy Bridge系列处理器中首次支持。AVX指令集在单指令多数据流计算性能增强的同时也沿用了的MMX/SSE指令集。不过和MMX/SSE的不同点在于增强的AVX指令,从指令的格式上就发生了很大的变化。x86(IA-32/Intel 64)架构的基础上增加了prefix(Prefix),所以实现了新的命令,也使更加复杂的指令得以实现,从而提升了x86 CPU的性能。
  4. AVX2指令集,2013年英特尔推出了包含AVX2的处理器。此架构增强了将AVX的打包整数功能从128位扩展到256位。AVX2指令集的一个重要更新是增加了乘加融合(FMA)指令,也添加了新的数据广播、混合和排列指令。
  5. AVX512指令集,2017年Intel 在 Skylake 体系结构中支持了AVX512 指令集。与AVX和AVX2不同,AVX512并不是一个不同的指令集扩展,而是一个相互关联的指令集扩展的集合。对于一个x86处理器,如果其支持AVX512F指令集扩展,那么它就是一个符合AVX512标准的处理器。符合AVX512标准的处理器可以选择性地支持附加的AVX512扩展,如高性能计算、服务器、桌面应用、移动服务等场景增加额外的扩展支持。

在 Linux 中,可以键入 lscpu 来查看 CPU 的基础信息,包括型号、代号、分级缓存信息和支持的指令集等。

1
2
3
4
[root@TENCENT64 ~]# lscpu
Architecture: x86_64
...
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single intel_ppin ssbd mba ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts pku ospke avx512_vnni md_clear flush_l1d arch_capabilities

考虑一下下面这个任务:计算一个很长的浮点型数组中每一个元素的平方根。实现这个任务的算法可以这样写:

1
2
for each f in array //对数组中的每一个元素
f = sqrt(f) //计算它的平方根

为了了解实现的细节,我们把上面的代码这样写:

1
2
3
4
5
6
for each f in array
{
把f从内存加载到浮点寄存器
计算平方根
再把计算结果从寄存器中取出放入内存
}

具有Intel SSE指令集支持的处理器有8个128位的寄存器,每一个寄存器可以存放4个(32位)单精度的浮点数。SSE同时提供了一个指令集,其中的指令可以允许把浮点数加载到这些128位的寄存器之中,这些数就可以在这些寄存器中进行算术逻辑运算,然后把结果放回内存。采用SSE技术后,算法可以写成下面的样子:

1
2
3
4
5
6
for each 4 members in array //对数组中的每4个元素
{
把数组中的这4个数加载到一个128位的SSE寄存器中
在一个CPU指令执行周期中完成计算这4个数的平方根的操作
把所得的4个结果取出写入内存
}

C++编程人员在使用SSE指令函数编程时不必关心这些128位的寄存器,你可以使用128位的数据类型“__m128”和一系列C++函数来实现这些算术和逻辑操作,而决定程序使用哪个SSE寄存器以及代码优化是C++编译器的任务。当需要对很长的浮点数数组中的元素进行处理的时候,SSE技术确实是一种很高效的方法。

下表不完全列举了 AVX512 的各种扩展指令集和对应的简要说明。

CPUID 标志 说明
AVX512F 基本指令集
AVX512ER 指数和倒数指令集
AVX512PF 预取指令集
AVX512CD 冲突检测指令集
AVX512DQ 双字和四字指令集
AVX512BW 字节和字指令集
AVX512VL 128位和256位向量指令集
AVX512_IFMA 整数融合乘加运算
AVX512_VBMI 附加向量字节指令集
AVX512_VNNI 向量神经网络指令集

向量寄存器

  1. SSE 和 AVX 各自有16个寄存器,SSE 的16个寄存器为 XMM0 - XMM15,XMM是128位寄存器,而YMM是256位寄存器。XMM寄存器也可以用于使用类似x86-SSE的单精度值或者双精度值执行标量浮点运算。
  2. 支持AVX的x86-64处理器包含16个256位大小的寄存器,名为YMM0 ~ YMM15。每个YMM寄存器的低阶128位的别名是相对应的XMM寄存器。大多数AVX指令可以使用任何一个XMM或者YMM寄存器作为SIMD操作数。
  3. AVX512 将每个AVX SIMD 寄存器的大小从256 位扩展到512位,称为ZMM寄存器;符合AVX512标准的处理器包含32个ZMM寄存器,名为ZMM0 ~ ZMM31。YMM 和 XMM 寄存器分别对应于每个ZMM寄存器的低阶 256 位和 128 位别名。AVX512 处理器还包括八个名为K0~K7的新的操作掩码寄存器;

img

图4 向量寄存器

SSE程序设计详细介绍

包含的头文件:

所有的SSE指令函数和__m128数据类型都在xmmintrin.h文件中定义:

1
#include <xmmintrin.h>

因为程序中用到的SSE处理器指令是由编译器决定,所以它并没有相关的.lib库文件。

数据分组(Data Alignment)

由SSE指令处理的每一个浮点数数组必须把其中需要处理的数每16个字节(128位二进制)分为一组。一个静态数组(static array)可由__declspec(align(16))关键字声明:

1
__declspec(align(16)) float m_fArray[ARRAY_SIZE];

动态数组(dynamic array)可由_aligned_malloc函数为其分配空间:

1
m_fArray = (float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16);

由_aligned_malloc函数分配空间的动态数组可以由_aligned_free函数释放其占用的空间:

1
_aligned_free(m_fArray);

__m128 数据类型

该数据类型的变量可用做SSE指令的操作数,它们不能被用户指令直接存取。_m128类型的变量被自动分配为16个字节的字长。

  1. SSE 有三种类型定义__m128,__m128d__m128i,分别用以表示单精度浮点型、双精度浮点型和整型。
  2. AVX/AVX2 有三种类型定义__m256,__m256d__m256i,分别用以表示单精度浮点型、双精度浮点型和整型。
  3. AVX512 有三种类型定义__m512,__m512d__512i,分别用以表示单精度浮点型、双精度浮点型和整型。
数据类型 描述 大小
__m128 包含4个单精度浮点数的128位向量 4 x 32 bit
__m128d 包含2个双精度浮点数的128位向量 2 x 64 bit
__m128i 包含数个整型数值的128位向量 128 bit
__m256 包含8个单精度浮点数的256位向量 8 x 32 bit
__m256d 包含4个双精度浮点数的256位向量 4 x 64 bit
__m256i 包含数个整型数值的256位向量 256 bit
__m512 包含16个单精度浮点数的512位向量 16 x 32 bit
__m512d 包含8个双精度浮点数的512位向量 8 x 64 bit
__m512i 包含数个整型数值的512位向量 512 bit

char, short, int, long 均属于整型。

img

图5 数据类型

编程实例

SSETest项目是一个基于对话框的应用程序,它用到了三个浮点数组参与运算:

1
fResult[i] = sqrt( fSource1[i]*fSource1[i] + fSource2[i]*fSource2[i] ) + 0.5

其中i = 0, 1, 2 … ARRAY_SIZE-1

其中ARRAY_SIZE被定义为30000。数据源数组(Source数组)通过使用sin和cos函数给它赋值,我们用Kris Jearakul开发的瀑布状图表控件(Waterfall chart control)[3] 来显示参与计算的源数组和结果数组。计算所需的时间(以毫秒ms为单位)在对话框中显示出来。我们使用三种不同的途径来完成计算:

  • 纯C++代码;
  • 使用SSE指令函数的C++代码;
  • 包含SSE汇编指令的代码。

 纯C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CSSETestDlg::ComputeArrayCPlusPlus(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{

int i;

float* pSource1 = pArray1;
float* pSource2 = pArray2;
float* pDest = pResult;

for ( i = 0; i < nSize; i++ )
{
*pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2) * (*pSource2)) + 0.5f;

pSource1++;
pSource2++;
pDest++;
}
}

下面我们用具有SSE特性的C++代码重写上面这个函数。

实现的功能 对应的SSE汇编指令 Visual C++.NET中的SSE函数
将4个32位浮点数放进一个128位的存储单元。 movss 和 shufps _mm_set_ps1
将4对32位浮点数同时进行相乘操作。这4对32位浮点数来自两个128位的存储单元,再把计算结果(乘积)赋给一个128位的存储单元。 mulps _mm_mul_ps
将4对32位浮点数同时进行相加操作。这4对32位浮点数来自两个128位的存储单元,再把计算结果(相加之和)赋给一个128位的存储单元。 addps _mm_add_ps
对一个128位存储单元中的4个32位浮点数同时进行求平方根操作。 sqrtps _mm_sqrt_ps

 使用Visual C++.NET的 SSE指令函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CSSETestDlg::ComputeArrayCPlusPlusSSE(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{
int nLoop = nSize/ 4;
__m128 m1, m2, m3, m4;
__m128* pSrc1 = (__m128*) pArray1;
__m128* pSrc2 = (__m128*) pArray2;
__m128* pDest = (__m128*) pResult;
__m128 m0_5 = _mm_set_ps1(0.5f); // m0_5[0, 1, 2, 3] = 0.5
for ( int i = 0; i < nLoop; i++ )
{
m1 = _mm_mul_ps(*pSrc1, *pSrc1); // m1 = *pSrc1 * *pSrc1
m2 = _mm_mul_ps(*pSrc2, *pSrc2); // m2 = *pSrc2 * *pSrc2
m3 = _mm_add_ps(m1, m2); // m3 = m1 + m2
m4 = _mm_sqrt_ps(m3); // m4 = sqrt(m3)
*pDest = _mm_add_ps(m4, m0_5); // *pDest = m4 + 0.5
pSrc1++;
pSrc2++;
pDest++;
}
}

使用SSE汇编指令实现的C++函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void CSSETestDlg::ComputeArrayAssemblySSE(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{
int nLoop = nSize/4;
float f = 0.5f;
_asm
{
movss xmm2, f // xmm2[0] = 0.5
shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0]
mov esi, pArray1 // 输入的源数组1的地址送往esi
mov edx, pArray2 // 输入的源数组2的地址送往edx
mov edi, pResult // 输出结果数组的地址保存在edi
mov ecx, nLoop //循环次数送往ecx

start_loop:
movaps xmm0, [esi] // xmm0 = [esi]
mulps xmm0, xmm0 // xmm0 = xmm0 * xmm0
movaps xmm1, [edx] // xmm1 = [edx]
mulps xmm1, xmm1 // xmm1 = xmm1 * xmm1
addps xmm0, xmm1 // xmm0 = xmm0 + xmm1
sqrtps xmm0, xmm0 // xmm0 = sqrt(xmm0)
addps xmm0, xmm2 // xmm0 = xmm1 + xmm2
movaps [edi], xmm0 // [edi] = xmm0
add esi, 16 // esi += 16
add edx, 16 // edx += 16
add edi, 16 // edi += 16
dec ecx // ecx--
jnz start_loop //如果不为0则转向start_loop
}
}

最后,在我的计算机上运行计算测试的结果:

  • 纯C++代码计算所用的时间是26 毫秒
  • 使用SSE的C++ 函数计算所用的时间是 9 毫秒
  • 包含SSE汇编指令的C++代码计算所用的时间是 9 毫秒

SSESample 示例项目

SSESample项目是一个基于对话框的应用程序,其中它用下面的浮点数数组进行计算:

1
fResult[i] = sqrt(fSource[i]*2.8)

其中i = 0, 1, 2 … ARRAY_SIZE-1

这个程序同时计算了数组中的最大值和最小值。

使用SSE汇编指令计算的结果会好一些,因为使用了效率增强了的SSX寄存器组。但是在通常情况下,使用SSE的C++ 函数计算会比汇编代码计算的效率更高一些,因为C++编译器的优化后的代码有很高的运算效率,若要使汇编代码比优化后的代码运算效率更高,这通常是很难做到的。

 纯C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 输入: m_fInitialArray
// 输出: m_fResultArray, m_fMin, m_fMax
void CSSESampleDlg::OnBnClickedButtonCplusplus()
{
m_fMin = FLT_MAX;
m_fMax = FLT_MIN;

int i;

for ( i = 0; i < ARRAY_SIZE; i++ )
{
m_fResultArray[i] = sqrt(m_fInitialArray[i] * 2.8f);

if ( m_fResultArray[i] < m_fMin )
m_fMin = m_fResultArray[i];

if ( m_fResultArray[i] > m_fMax )
m_fMax = m_fResultArray[i];
}
}

 使用Visual C++.NET的 SSE指令函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 输入: m_fInitialArray
// 输出: m_fResultArray, m_fMin, m_fMax
void CSSESampleDlg::OnBnClickedButtonSseC()
{
__m128 coeff = _mm_set_ps1(2.8f); // coeff[0, 1, 2, 3] = 2.8
__m128 tmp;

__m128 min128 = _mm_set_ps1(FLT_MAX); // min128[0, 1, 2, 3] = FLT_MAX
__m128 max128 = _mm_set_ps1(FLT_MIN); // max128[0, 1, 2, 3] = FLT_MIN

__m128* pSource = (__m128*) m_fInitialArray;
__m128* pDest = (__m128*) m_fResultArray;

for ( int i = 0; i < ARRAY_SIZE/4; i++ )
{
tmp = _mm_mul_ps(*pSource, coeff); // tmp = *pSource * coeff
*pDest = _mm_sqrt_ps(tmp); // *pDest = sqrt(tmp)

min128 = _mm_min_ps(*pDest, min128);
max128 = _mm_max_ps(*pDest, max128);

pSource++;
pDest++;
}

// 计算max128的最大值和min128的最小值
union u
{
__m128 m;
float f[4];
} x;

x.m = min128;
m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3])));

x.m = max128;
m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3])));
}

 使用SSE汇编指令的C++函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    // 输入: m_fInitialArray
// 输出: m_fResultArray, m_fMin, m_fMax
void CSSESampleDlg::OnBnClickedButtonSseAssembly()
{

float* pIn = m_fInitialArray;
float* pOut = m_fResultArray;

float f = 2.8f;
float flt_min = FLT_MIN;
float flt_max = FLT_MAX;

__m128 min128;
__m128 max128;

// 使用以下的附加寄存器:xmm2、xmm3、xmm4:
// xmm2 – 相乘系数
// xmm3 – 最小值
// xmm4 – 最大值

_asm
{
movss xmm2, f // xmm2[0] = 2.8
shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0]

movss xmm3, flt_max // xmm3 = FLT_MAX
shufps xmm3, xmm3, 0 // xmm3[1, 2, 3] = xmm3[0]

movss xmm4, flt_min // xmm4 = FLT_MIN
shufps xmm4, xmm4, 0 // xmm3[1, 2, 3] = xmm3[0]

mov esi, pIn // 输入数组的地址送往esi
mov edi, pOut // 输出数组的地址送往edi
mov ecx, ARRAY_SIZE/4 // 循环计数器初始化

start_loop:
movaps xmm1, [esi] // xmm1 = [esi]
mulps xmm1, xmm2 // xmm1 = xmm1 * xmm2
sqrtps xmm1, xmm1 // xmm1 = sqrt(xmm1)
movaps [edi], xmm1 // [edi] = xmm1

minps xmm3, xmm1
maxps xmm4, xmm1

add esi, 16
add edi, 16

dec ecx
jnz start_loop

movaps min128, xmm3
movaps max128, xmm4
}

union u
{
__m128 m;
float f[4];
} x;

x.m = min128;
m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3])));

x.m = max128;
m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3])));
}

SIMD

SSE(为Streaming SIMD Extensions的缩写)是由 Intel公司,在1999年推出Pentium III处理器时,同时推出的新指令集。如同其名称所表示的,SSE是一种SIMD指令集。SSE有8个128位寄存器,XMM0 ~XMM7。这些128位元的寄存器,可以用来存放四个32位的单精确度浮点数。SSE的浮点数运算指令就是使用这些寄存器。

SSE寄存器结构如下:

寄存器与指令数据细节

在MMX指令集中,使用的寄存器称作MM0到MM7,实际上借用了浮点处理器的8个寄存器的低64Bit,这样导致了浮点运算速度降低。

SSE指令集推出时,Intel公司在Pentium III CPU中增加了8个128位的SSE指令专用寄存器,称作XMM0到XMM7。这样SSE指令寄存器可以全速运行,保证了与浮点运算的并行性。这些XMM寄存器用于4个单精度浮点数运算的SIMD执行,并可以与MMX整数运算或x87浮点运算混合执行。

2001年在Pentium 4上引入了SSE2技术,进一步扩展了指令集,使得XMM寄存器上可以执行8/16/32位宽的整数SIMD运算或双精度浮点数的SIMD运算。对整型数据的支持使得所有的MMX指令都是多余的了,同时也避免了占用浮点数寄存器。SSE2为了更好地利用高速寄存器,还新增加了几条寄存指令,允许程序员控制已经寄存过的数据。这使得 SIMD技术基本完善。

SSE3指令集扩展的指令包含寄存器的局部位之间的运算,例如高位和低位之间的加减运算;浮点数到整数的转换,以及对超线程技术的支持。

AVX是Intel的SSE延伸架构,把寄存器XMM 128bit提升至YMM 256bit,以增加一倍的运算效率。此架构支持了三运算指令(3-Operand Instructions),减少在编码上需要先复制才能运算的动作。在微码部分使用了LES LDS这两少用的指令作为延伸指令Prefix。AVX的256bit的YMM寄存器分为两个128bit的lanes,AVX指令并不支持跨lanes的操作。其中YMM寄存器的低128位与Intel SSE指令集的128bitXMM寄存器复用。尽管VGX并不要求内存对齐,但是内存对齐有助于提升性能。如对于128-bit访问的16字节对齐和对于256-bit访问的32字节对齐。

AVX虽然已经将支持的SIMD数据宽度增加到了256位,但仅仅增加了对256位的浮点SIMD支持,整点SIMD数据的宽度还停留在128位上,AVX2支持的整点SIMD数据宽度从128位扩展到256位。同时支持了跨lanes操作,加入了增强广播、置换指令支持的数据元素类型、移位操作对各个数据元素可变移位数的支持、跨距访存支持。AVX硬件由16个256bitYMM寄存器(YMM0~YMM15)组成。

每一代的指令集都是对上一代兼容的,支持上一代的指令,也可以使用上一代的寄存器,也就是说,AVX2也依然支持128位,64位的操作,也可以使用上一代的寄存器(当然,寄存器的硬件实现可能有区别)。AVX也对部分之前的指令接口进行了重构,所以可以在指令文档中找到几个处于不同代际有着相同功能调用接口却不相同的函数。

另外,不同代际的指令不要混用,每次状态切换将消耗 50-80 个时钟周期,会拖慢程序的运行速度。

数据结构

由于通常没有内建的128bit和256bit数据类型,SIMD指令使用自己构建的数据类型,这些类型以union实现,这些数据类型可以称作向量,一般来说,MMX指令是__m64 类型的数据,SSE是__m128类型的数据等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef union __declspec(intrin_type) _CRT_ALIGN(8) __m64
{
unsigned __int64 m64_u64;
float m64_f32[2];
__int8 m64_i8[8];
__int16 m64_i16[4];
__int32 m64_i32[2];
__int64 m64_i64;
unsigned __int8 m64_u8[8];
unsigned __int16 m64_u16[4];
unsigned __int32 m64_u32[2];
} __m64;

typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 {
float m128_f32[4];
unsigned __int64 m128_u64[2];
__int8 m128_i8[16];
__int16 m128_i16[8];
__int32 m128_i32[4];
__int64 m128_i64[2];
unsigned __int8 m128_u8[16];
unsigned __int16 m128_u16[8];
unsigned __int32 m128_u32[4];
} __m128;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128i {
__int8 m128i_i8[16];
__int16 m128i_i16[8];
__int32 m128i_i32[4];
__int64 m128i_i64[2];
unsigned __int8 m128i_u8[16];
unsigned __int16 m128i_u16[8];
unsigned __int32 m128i_u32[4];
unsigned __int64 m128i_u64[2];
} __m128i;

typedef struct __declspec(intrin_type) _CRT_ALIGN(16) __m128d {
double m128d_f64[2];
} __m128d;
数据类型 描述
__m128 包含4个float类型数字的向量
__m128d 包含2个double类型数字的向量
__m128i 包含若干个整型数字的向量
__m256 包含8个float类型数字的向量
__m256d 包含4个double类型数字的向量
__m256i 包含若干个整型数字的向量

每一种类型,从2个下划线开头,接一个m,然后是向量的位长度。如果向量类型是以d结束的,那么向量里面是double类型的数字。如果没有后缀,就代表向量只包含float类型的数字。整形的向量可以包含各种类型的整形数,例如char,short,unsigned long long。也就是说,__m256i可以包含32个char,16个short类型,8个int类型,4个long类型。这些整形数可以是有符号类型也可以是无符号类型。

内存对齐

为了方便CPU用指令对内存进行访问,通常要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数,如果一个变量的内存地址正好位于它长度的整数倍,我们就称他是自然对齐的。不同长度的内存访问会用到不同的汇编指令,这种对齐限制简化了形成处理器和存储器系统之间接口的硬件设计,提高了内存的访问效率。

通常对于各种类型的对齐规则如下:

  • 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
  • 联合 :按其包含的长度最大的数据类型对齐。
  • 结构体: 结构体中每个数据类型都要对齐

对于SIMD的内存对齐是指__m128等union在内存中存储时的存储方式。然而由于结构内存对齐的规则略微复杂,我们以结构为例进行说明:

一般情况下,由于内存对齐的原因存储多种类型数据的结构体所占的内存大小并非元素本身类型大小之和。对于自然对齐而言:

对于各成员变量来说,存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数,各成员变量在存放的时候根据在结构中出现的顺序依次申请空间, 同时按照上面的对齐方式调整位置, 空缺的字节自动填充。

对于整个结构体来说,为了确保结构的大小为结构的字节边界数(即该结构中占用最大的空间的类型的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

所以一般我们在定义结构体时定义各元素的顺序也会影响实际结构体在存储时的整体大小,把大小相同或相近的元素放一起,可以减少结构体占用的内存空间。

除了自然对齐的内存大小,我们也可以设置自己需要的对齐大小,我们称之为对齐系数,如果结构内最大类型的字节数小于对齐系数,结构体内存大小应按最大元素大小对齐,如果最大元素大小超过对齐系数,应按对齐系数大小对齐。

对齐系数大小的设定可以使用下列方法:

#pragma pack (16)使用预编译器指令要求对齐。#pragma pack()恢复为默认对齐方式。

1
__attribute__ ((aligned (16)))//GCC要求对齐

1
__declspec(intrin_type) _CRT_ALIGN(16)//Microsoft Visual C++要求对齐

联合的内存对齐方式与结构类似。

SIMD的指令中通常有对内存对齐的要求,例如,SSE中大部分指令要求地址是16bytes对齐的,以_mm_load_ps函数来说明,这个函数对应于SSE的loadps指令。

函数原型为:extern __m128 _mm_load_ps(float const*_A);

可以看到,它的输入是一个指向float的指针,返回的就是一个__m128类型的数据,从函数的角度理解,就是把一个float数组的四个元素依次读取,返回一个组合的__m128类型的SSE数据类型,从而可以使用这个返回的结果传递给其它的SSE指令进行运算,比如加法等;从汇编的角度理解,它对应的就是读取内存中连续四个地址的float数据,将其放入SSE的寄存器(XMM)中,从而给其他的指令准备好数据进行计算。其使用示例如下:

1
2
float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
__m128 a = _mm_load_ps(input);|//WARNING

这里加载正确的前提是:input这个浮点数阵列都是对齐在16 bytes的边上。否则程序会崩溃或得不到正确结果。如果没有对齐,就需要使用_mm_loadu_ps函数,这个函数用于处理没有对齐在16bytes上的数据,但是其速度会比较慢。

对于上面的例子,如果要将input指定为16bytes对齐,可以采用的方式是:

1
__declspec(align(16)) float input[4] = {1.0, 2.0, 3.0, 4.0};

为了简化,头文件<xmmintrin.h>中定义了一个宏_MM_ALIGN16来表示上面的含义,即可以用:

1
_MM_ALIGN16 float input[4] = {1.0, 2.0, 3.0, 4.0};

256-bit AVX 指令在内存访问上对内存对齐比128-bit SSE 指令有更高要求。虽然在一个cache-line 之内,Intel 的对齐和非对齐指令已经没有性能差距了,但是由于AVX 有更长的内存访问宽度(YMM <-> memory),会更频繁地触及cache-line 边界。所以1)尽量使用对齐内存分配;2)有时候内存对齐不能保证,可以用128-bit(XMM)指令访问内存,然后再组合成256-bit YMM

工作模式

SSE的浮点运算指令分为两大类:Packed 和Scalar。Packed指令是一次对XMM寄存器中的四个浮点数(即DATA0 ~ DATA3)均进行计算,而Scalar则只对XMM暂存器中的DATA0进行计算。如下图所示:

下面是SSE指令的一般格式,由三部分组成,第一部分是表示指令的作用,比如加法add等,第二部分是s或者p分别表示scalar或packed,第三部分为s,表示单精度浮点数(single precision floating point data)。

根据上面知道,SSE的寄存器是128bit的,那么SSE就需要使用128bit的数据类型,SSE使用4个浮点数(4*32bit)组合成一个新的数据类型,用于表示128bit类型,SSE指令的返回结果也是128bit的。

SSE 指令和一般的x86 指令很类似,基本上包括两种定址方式:寄存器-寄存器方式(reg-reg)和寄存器-内存方式(reg-mem):

1
2
addps xmm0, xmm1 ; reg-reg
addps xmm0, [ebx] ; reg-mem

SSE中大部分指令要求地址是16byte对齐的。要理解这个问题,以_mm_load_ps函数来解释,这个函数对应于loadps的SSE指令。其原型为:extern __m128 _mm_load_ps(float const*_A);

可以看到,它的输入是一个指向float的指针,返回的就是一个__m128类型的数据,从函数的角度理解,就是把一个float数组的四个元素依次读取,返回一个组合的__m128类型的SSE数据类型,从而可以使用这个返回的结果传递给其它的SSE指令进行运算,比如加法等;从汇编的角度理解,它对应的就是读取内存中连续四个地址的float数据,将其放入SSE新的暂存器(XMM0~8)中,从而给其他的指令准备好数据进行计算。其使用示例如下:

1
2
float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };  
__m128 a = _mm_load_ps(input);

这里加载正确的前提是:input这个浮点数数组是对齐在16 byte的边上。否则加载的结果和预期的不一样。如果没有对齐,就需要使用_mm_loadu_ps函数,这个函数用于处理没有对齐在16byte上的数据,但是其速度会比较慢。

这个只是使用SSE指令的时候要注意一下,我们知道,x86的little-endian特性,位址较低的byte会放在暂存器的右边。也就是说,若以上面的input为例,在载入到XMM暂存器后,暂存器中的DATA0会是1.0,而DATA1是2.0,DATA2是3.0,DATA3是4.0。如果需要以相反的顺序载入的话,可以用_mm_loadr_ps 这个intrinsic,根据需要进行选择。

环境配置

使用软件CPU-Z可以查看CPU支持的指令集。

我们可以在C/C++使用封装的函数而不是嵌入的汇编代码的方式来调用指令集,这就是Compiler Intrinsics。

Intrinsics指令是对MMX、SSE等指令集的指令的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。

除了我们这里使用的intrinsics指令,还有intrinsics函数需要以作区分,这两者既有联系又有区别。编译器指令#pragma intrinsic()可以将一些指定的系统库函数编译为内部函数,从而去掉函数调用参数传递等的开销,这种方式只适用于编译器规定的一部分函数,不是所有函数都能使用,同时会增大生成代码的大小。

intrinsics更广泛的使用是指令集的封装,能将函数直接映射到高级指令集,同时隐藏了寄存器分配和调度等,从而使得程序员可以以函数调用的方式来实现汇编能达到的功能,编译器会生成为对应的SSE等指令集汇编。

Intel Intrinsic Guide可以查询到所有的Intrinsic指令、对应的汇编指令以及如何使用等。

对于VC来说,VC6支持MMX、3DNow!、SSE、SSE2,然后更高版本的VC支持更多的指令集。但是,VC没有提供检测Intrinsic函数集支持性的办法。

而对于GCC来说,它使用-mmmx、-msse等编译器开关来启用各种指令集,同时定义了对应的__MMX____SSE__等宏,然后x86intrin.h会根据这些宏来声明相应的Intrinsic函数集。__MMX____SSE__等宏可以帮助我们判断Intrinsic函数集是否支持,但这只是GCC的专用功能。

如果使用GCC编译器时,使用intrinsics指令时需要在编写cmake或者makefile文件时加上相关参数,例如使用AVX指令集时添加-mavx2参数。

GCC:

头文件 编译器参数
avx2intrin.h AVX2 -mavx2
avxintrin.h AVX -mavx
emmintrin.h SSE2 -msse2
nmmintrin.h SSE4_2 -msse4.2
xmmintrin.h SSE -msse
mmintrin.h MMX -mmmx

头文件设置

1
2
3
4
5
6
7
8
9
10
#include <mmintrin.h> //MMX
#include <xmmintrin.h> //SSE(include mmintrin.h)
#include <emmintrin.h> //SSE2(include xmmintrin.h)
#include <pmmintrin.h> //SSE3(include emmintrin.h)
#include <tmmintrin.h>//SSSE3(include pmmintrin.h)
#include <smmintrin.h>//SSE4.1(include tmmintrin.h)
#include <nmmintrin.h>//SSE4.2(include smmintrin.h)
#include <wmmintrin.h>//AES(include nmmintrin.h)
#include <immintrin.h>//AVX(include wmmintrin.h)
#include <intrin.h>//(include immintrin.h)

上述头文件中,下一个头文件包含上一个头文件中内容,例如xmmintrin.h为SSE 头文件,此头文件里包含MMX头文件,emmintrin.h为SSE2头文件,此头文件里包含SSE头文件。

VC引入<intrin.h>会自动引入当前编译器所支持的所有Intrinsic头文件。GCC引入<x86intrin.h>.

使用

使用SSE指令,首先要了解这一类用于进行初始化加载数据以及将寄存器的数据保存到内存相关的指令,我们知道,大多数SSE指令是使用的xmm0到xmm8的寄存器,那么使用之前,就需要将数据从内存加载到这些寄存器,在寄存器中完成运算后, 再把计算结果从寄存器中取出放入内存。C++编程人员在使用SSE指令函数编程时,除了加载存储数据外,不必关心这些128位的寄存器的调度,你可以使用128位的数据类型__m128和一系列C++函数来实现这些算术和逻辑操作,而决定程序使用哪个SSE寄存器以及代码优化是C++编译器的任务。

load系列函数,用于加载数据,从内存到寄存器。

set系列函数,用于加载数据,大部分需要多个指令执行周期完成,但是可能不需要16字节对齐.这一系列函数主要是类似于load的操作,但是可能会调用多条指令去完成,方便的是可能不需要考虑对齐的问题。

store系列函数,用于将计算结果等SSE寄存器的数据保存到内存中。这一系列函数和load系列函数的功能对应,基本上都是一个反向的过程

SSE 指令和 AVX 指令混用
SSE/AVX 的混用有时不可避免,AVX-SSE transition penalty并不是由混合SSE和AVX指令导致的,而是因为混合了legacy SSE encoding 和 VEX encoding。

所以在使用Intel intrinsic写全新的程序时其实并不需要太担心这个问题,因为只要指定了合适的CPU 架构(比如-mavx),SSE 和AVX intrinsic 都会被编译器生成VEX-encoding 代码。

函数命名

SIMD指令的intrinsics函数名称一般为如下形式,

1
_mm<bit_width>_<name>_<data_type>

表明了向量的位长度,即操作对象的数据类型大小,对于128位的向量,这个参数为空,对于256位的向量,这个参数为256。

描述了内联函数的算术操作。一般由两部分组成:

第一部分是表示指令的作用,比如加法add等;

第二部分是可选的修饰符,表示一些特殊的作用,比如从内存对齐,逆序加载等;

表明了操作的粒度,具体情形见下表:

标识 数据类型
epi8/epi16/epi32 有符号的8,16,32位整数
epu8/epu16/epu32 无符号的8,16.32位整数
si128/si256 未指定的128,256位向量
ps 包装型单精度浮点数
ss scalar single precision floating point data 数量型单精度浮点数
pd pached double precision floating point data 包装型双精度浮点数
sd 数量型双精度浮点数
可选的修饰符 示例 描述
u loadu Unaligned memory: 对内存未对齐的数据进行操作
s subs/adds Saturate: 饱和计算将考虑内存能够存储的最小/最大值。非饱和计算略内存问题。即计算的上溢和下溢
h hsub/hadd Horizontally: 在水平方向上做加减法
hi/lo mulhi 高/低位
r setr Reverse order: 逆序初始化向量
fm fmadd Fused-Multiply-Add(FMA)运算,单一指令进行三元运算

在饱和模式下,当计算结果发生溢出(上溢或下溢)时,CPU会自动去掉溢出的部分,使计算结果取该数据类型表示数值的上限值(如果上溢)或下限值(如果下溢)。

注释中的printf部分是利用__m128这个数据类型来获取相关的值,这个类型是一个union类型,具体定义可以参考相关头文件,但是,对于实际使用,有时候这个值是一个中间值,需要后面计算使用,就得使用store了,效率更高。上面使用的是_mm_loadu_ps_mm_storeu_ps,不要求字节对齐,如果使用_mm_load_ps_mm_store_ps,会发现程序会崩溃或得不到正确结果。下面是指定字节对齐后的一种实现方法:

这类函数名一般以__m开头。函数名称和指令名称有一定的关系

SSE/AVX 指令集允许使用汇编指令集去操作XMM和YMM寄存器,但直接使用AVX 汇编指令编写汇编代码并不是十分友好而且效率低下。因此,intrinsic function 应运而生。Intrinsic function 类似于 high level 的汇编,开发者可以无痛地将 instinsic function 同 C/C++ 的高级语言特性(如分支、循环、函数和类)无缝衔接。

SSE/AVX intrinsic functions 的命名习惯如下

1
__<return_type> _<vector_size>_<intrin_op>_<suffix>

__128i, _256i是由整型构成的向量,char、 short、 int 、 long 均属于整型。

1
2
3
4
5
__m128 _mm_set_ps (float e3, float e2, float e1, float e0)

__m256 _mm256_add_pd (__m256 a, __m256 b)

__m512 _mm512_max_epi64 (__m512 a, __m512 b)
  1. return_type, 如 m128、m256 和 m512 代表函数的返回值类型,m128 代表128位的向量,m256代表256位的向量,m512代表512位的向量。
  2. vector_size , 如 mm、mm256 和 mm512 代表函数操作的数据向量的位长度,mm 代表 128 位的数据向量(SSE),mm256 代表256位的数据向量(AVX 和 AVX2), mm512 代表512位的数据向量。
  3. intrin_op,如 set、add 和 max 非常直观的解释函数功能。函数基础功能可以分为数值计算、数据传输、比较和转型四种,参阅 Intel Intrinsics Guidex86 Intrinsics Cheat Sheet
  4. suffix, 如ps、pd、epi64代表函数参数的数据类型,其中 p = packed,s = 单精度浮点数,d = 双精度浮点数,ep
  • ps: 由float类型数据组成的向量
  • pd:由double类型数据组成的向量
  • epi8/epi16/epi32/epi64: 由8位/16位/32位/64位的有符号整数组成的向量
  • epu8/epu16/epu32/epu64: 包含8位/16位/32位/64位的无符号整数组成的向量
  • si128/si256: 未指定的128位或者256位向量

常用的 Intrinsic 指令

在理解了最基础的指令后,可以到 Intel Intrinsic Guide 查询到所有指令。

1、 load系列,用于加载数据,从内存到暂存器。

1
2
3
4
5
6
7
__m128 _mm_load_ss (float *p)  
__m128 _mm_load_ps (float *p)
__m128 _mm_load1_ps (float *p)
__m128 _mm_loadh_pi (__m128 a, __m64 *p)
__m128 _mm_loadl_pi (__m128 a, __m64 *p)
__m128 _mm_loadr_ps (float *p)
__m128 _mm_loadu_ps (float *p)

上面是从手册查询到的load系列的函数。其中,

  • _mm_load_ss用于scalar的加载,所以,加载一个单精度浮点数到暂存器的低字节,其它三个字节清0,(r0 := *p, r1 := r2 := r3 := 0.0)。
  • _mm_load_ps用于packed的加载(下面的都是用于packed的),要求p的地址是16字节对齐,否则读取的结果会出错,(r0 := p[0], r1 := p[1], r2 := p[2], r3 := p[3])。
  • _mm_load1_ps表示将p地址的值,加载到暂存器的四个字节,需要多条指令完成,所以,从性能考虑,在内层循环不要使用这类指令。(r0 := r1 := r2 := r3 := *p)。
  • _mm_loadh_pi_mm_loadl_pi分别用于从两个参数高底字节等组合加载。具体参考手册。
  • _mm_loadr_ps表示以_mm_load_ps反向的顺序加载,需要多条指令完成,当然,也要求地址是16字节对齐。(r0 := p[3], r1 := p[2], r2 := p[1], r3 := p[0])。
  • _mm_loadu_ps_mm_load_ps一样的加载,但是不要求地址是16字节对齐,对应指令为movups。

2、set系列,用于加载数据,大部分需要多条指令完成,但是可能不需要16字节对齐。

1
2
3
4
5
__m128 _mm_set_ss (float w)  
__m128 _mm_set_ps (float z, float y, float x, float w)
__m128 _mm_set1_ps (float w)
__m128 _mm_setr_ps (float z, float y, float x, float w)
__m128 _mm_setzero_ps ()

这一系列函数主要是类似于load的操作,但是可能会调用多条指令去完成,方便的是可能不需要考虑对齐的问题。

  • _mm_set_ss对应于_mm_load_ss的功能,不需要字节对齐,需要多条指令。(r0 = w, r1 = r2 = r3 = 0.0)
  • _mm_set_ps对应于_mm_load_ps的功能,参数是四个单独的单精度浮点数,所以也不需要字节对齐,需要多条指令。(r0=w, r1 = x, r2 = y, r3 = z,注意顺序)
  • _mm_set1_ps对应于_mm_load1_ps的功能,不需要字节对齐,需要多条指令。(r0 = r1 = r2 = r3 = w)
  • _mm_setzero_ps是清0操作,只需要一条指令。(r0 = r1 = r2 = r3 = 0.0)

3、store系列,用于将计算结果等SSE寄存器的数据保存到内存中。

1
2
3
4
5
6
7
8
void _mm_store_ss (float *p, __m128 a)  
void _mm_store_ps (float *p, __m128 a)
void _mm_store1_ps (float *p, __m128 a)
void _mm_storeh_pi (__m64 *p, __m128 a)
void _mm_storel_pi (__m64 *p, __m128 a)
void _mm_storer_ps (float *p, __m128 a)
void _mm_storeu_ps (float *p, __m128 a)
void _mm_stream_ps (float *p, __m128 a)

这一系列函数和load系列函数的功能对应,基本上都是一个反向的过程。

  • _mm_store_ss:一条指令,*p = a0
  • _mm_store_ps:一条指令,p[i] = a[i]。
  • _mm_store1_ps:多条指令,p[i] = a0。
  • _mm_storeh_pi,_mm_storel_pi:值保存其高位或低位。
  • _mm_storer_ps:反向,多条指令。
  • _mm_storeu_ps:一条指令,p[i] = a[i],不要求16字节对齐。
  • _mm_stream_ps:直接写入内存,不改变cache的数据。

4、算术指令

SSE提供了大量的浮点运算指令,包括加法、减法、乘法、除法、开方、最大值、最小值、近似求倒数、求开方的倒数等等,可见SSE指令的强大之处。那么在了解了上面的数据加载和数据保存的指令之后,使用这些算术指令就很容易了,下面以加法为例。SSE中浮点加法的指令有:

1
2
__m128 _mm_add_ss (__m128 a, __m128 b)  
__m128 _mm_add_ps (__m128 a, __m128 b)

其中,_mm_add_ss表示scalar执行模式,_mm_add_ps表示packed执行模式。

一般而言,使用SSE指令写代码,步骤为:使用load/set函数将数据从内存加载到SSE暂存器;使用相关SSE指令完成计算等;使用store系列函数将结果从暂存器保存到内存,供后面使用。

_mm_prefetch

1
void_mm_prefetch(char *p, int i)

从地址P处预取尺寸为cache line大小的数据缓存,参数i指示预取方式(_MM_HINT_T0, _MM_HINT_T1, _MM_HINT_T2, _MM_HINT_NTA,分别表示不同的预取方式)

  • T0 预取数据到所有级别的缓存,包括L0。
  • T1 预取数据到除L0外所有级别的缓存。
  • T2 预取数据到除L0和L1外所有级别的缓存。
  • NTA 预取数据到非临时缓冲结构中,可以最小化对缓存的污染。

如果在CPU操作数据之前,我们就已经将数据主动加载到缓存中,那么就减少了由于缓存不命中,需要从内存取数的情况,这样就可以加速操作,获得性能上提升。使用主动缓存技术来优化内存拷贝。

注 意,CPU对数据操作拥有绝对自由!使用预取指令只是按我们自己的想法对CPU的数据操作进行补充,有可能CPU当前并不需要我们加载到缓存的数据,这 样,我们的预取指令可能会带来相反的结果,比如对于多任务系统,有可能我们冲掉了有用的缓存。不过,在多任务系统上,由于线程或进程的切换所花费的时间相 对于预取操作来说太长了, 所以可以忽略线程或进程切换对缓存预取的影响。

_mm_movehl_ps

Moves the upper two single-precision, floating-point values of b to the lower two single-precision, floating-point values of the result. The upper two single-precision, floating-point values of a are passed through to the result.

将 b 的高 64 位移至结果的低 64 位, a 的高 64 位传递给结果。

如:

1
2
3
r = __m128 _mm_movehl_ps( __m128 a, __m128 b ); //r = {a3, a2, b3, b2} // 高 — 低

s = _mm_movehl_ps( x , x );// 高-- 低s = {x3, x2, x3, x2}

关于指令集的一些问题集中回答

几个问题

(1)浮点计算 vs 整数计算:为什么要分开讲呢?因为在指令集中也是分开的,另外,由于浮点数占4个字节或者8个字节,而整数却可以分别占1,2,4个字节按照应用场合不同使用的不同,因此向量化加速也不同。因此一个指令最多完成4个浮点数计算。而可以完成16个int8_t数据的计算。

(2)优化技巧:注意指令的顺序,为什么呢,因为CPU是流水线工作的,因此相邻的指令开始的执行的时间并非一个指令执行完毕之后才会开始,但是一旦遇到数据联系,这时候会发生阻塞,如果我们很好的安排指令的顺序,使得数据相关尽量少发生,或者发生的时候上一个指令已经执行完了。因此注意稍微修改指令的执行顺序就会使得代码变快。

指令集的一些问题

(1)没有统一的移植标准。

就以SSE指令而言。SSE的指令集是X86架构CPU特有的,对于ARM架构、MIPS架构等CPU是不支持的,所以使用了SSE指令集的程序,是不具备可移植标准的。

不仅如此,前面说过Intel和AMD对于同样的128bit向量的指令语法是不一样的,所以,在Intel之下所写的代码并不能一直到AMD的机器上进行指令集加速,其它的也一样,也就是说,写的某一种指令加速代码,不具备完全的可移植性。

SIMD指令,可以一次性装载多个元素到寄存器。如果是128位宽度,则可以一次装载4个单精度浮点数。这4个float可以一次性地参与乘法计算,理论上可提速4倍。不同的平台有不同的SIMD指令集,如Intel平台的指令集有MMX、SSE、AVX2、AVX512等(后者是对前者的扩展,本质一样),ARM平台是128位的NEON指令集。如果你希望用SIMD给算法加速,你首先需要学习不同平台的SIMD指令集,并为不同的平台写不同的代码,最后逐个测试准确性。这样无法实现write once, run anywhere的目标。

(2)针对指令集没办法转移的解决方案

OpenCV 4.x中提供了强大的统一向量指令(universal intrinsics),使用这些指令可以方便地为算法提速。所有的计算密集型任务皆可使用这套指令加速,并不是专门针对计算机视觉算法。目前OpenCV的代码加速实现基本上都基于这套指令。OpenCV设计了一套统一的向量指令universal intrinsics,可以让你写一份代码,在不同平台上都可以实现向量加速

指令集优化代码的一般步骤

  1. 第一步:即所谓的load步骤。指的是需要将数据从内存加载(load)到CPU的内存储里面;
  2. 第二步:即所谓的运算。将加载进来的数据进行加减乘除等等运算;
  3. 第三步:即所谓的store步骤。将运算的结果需要重新存储到内存里面;

SSE指令集的使用说明

SSE本质上类似于一个向量处理器,所谓的向量处理器实际上就是进行向量的运算,

包括了4个主要部分:单精确度浮点数运算指令、整数运算指令(为MMX的延伸,并与MMX使用同样的暂存器)、Cache控制指令、状态控制指令。

如何使用SSE指令

使用SSE指令有两种方式:

  • 一是直接在C/C++中嵌入(汇编)指令;
  • 二是使用Intel C++ Compiler或是Microsoft Visual C++中提供的支持SSE指令集的intrinsics内联函数。

从代码可读和维护角度讲,推荐使用intrinsics内联函数的形式。intrinsics是对MMX、SSE等指令集的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。想要使用SSE指令,则需要包含对应的头文件:

1
2
3
4
#include <mmintrin.h> //mmx
#include <xmmintrin.h> //sse
#include <emmintrin.h> //sse2
#include <pmmintrin.h> //sse3

备注:本文所介绍的是在VS平台中VC++所提供的intrinstic内联函数的使用说明。这样使用起来就很简单了,主要是包含两部分,数据类型和数据操作指令(加载load、运算、存储store),另外,虽然现在SSE已经有了很多个版本,SSE、SSE2、SSE3、SSE4.1、SSSE4.2等等,它们之间有所差别,但是大致的使用以及思想原理是一致的。

SSE的数据类型

SSE指令中intrinsics函数的数据类型为:

(1)__m128(单精度浮点数),如果使用sizeof(__m128)计算该类型大小,结果为16,即等于四个浮点数长度。__declspec(align(16))做为数组定义的修释符,表示该数组是以16字节为边界对齐的,因为SSE指令大部分支持这种格式的内存数据。他的定义如下:

1
2
3
typedef struct __declspec(intrin_type) __declspec(align(16)) __m128 {
float m128_f32[4];
} __m128;

__m128外、还包括

(2)__m128d(双精度浮点数)

(3)__m128i(整型)。其中__m128i是一个共用体类型(union),其定义如下 :

1
2
3
4
5
6
7
8
9
10
typedef union __declspec(intrin_type) __declspec(align(16)) __m128i {
__int8 m128i_i8[16];
__int16 m128i_i16[8];
__int32 m128i_i32[4];
__int64 m128i_i64[2];
unsigned __int8 m128i_u8[16];
unsigned __int16 m128i_u16[8];
unsigned __int32 m128i_u32[4];
unsigned __int64 m128i_u64[2];
} __m128i;

注意数据类型前面是两个短的下划线哦!!!

数据操作指令的一般格式(包括了数据加载load、数据运算、数据存储store)

SSE指令通常由三部分构成:

  • 第一部分为前缀_mm(多媒体扩展指令集),表示该函数属于SSE指令集(前面只有一个短下划线)
  • 第二部分为指令的操作类型,
    • 如加载数据一般是_load以及它的变种
    • 如_add、_mul等以及这些运算的变种(一个短下划线)
    • 存储数据_store以及它的一些变种
  • 第三部分通常由一个短下划线加上两个字母组成。
    • 第一个字母表示对结果变量的影响方式,为p或s。
      • p(packed:包裹指令) :该指令对xmm寄存器中的每个元素进行运算,即一次对四个浮点数(data0~data3)均进行计算;
      • s(scalar:标量指令):该指令对寄存器中的第一个元素进行运算,即一次只对xmm寄存器中的data0进行计算。
      • 如果针对SSE的四个数所组成的向量,如果是packed模式,则进行向量运算,如果是scalar模式,只会对第一组数据进行运算。
    • 第二个字母表示参与运算的数据类型,
      • s表示32位浮点数,
      • d表示64位浮点数,
      • i32表示带符号32位整型,
      • i64表示带符号64位整型,
      • u32表示无符号32位整型,
    • 第三部分还可以是_pi**格式或者是_*pi**格式。
      • _pi****为长度,可以是8,16,32,64)packed操作所有的**位有符号整数,使用的寄存器长度为64位;
      • _epi****为长度)packed操作所有的**位的有符号整数,使用的寄存器长度为128位;
      • _epu**同样的道理 packed操作所有的**位的无符号整数;

以此类推。由于SSE只支持32位浮点数的运算,所以你可能会在这些指令封装函数中找不到包含非s修饰符的,但你可以在MMX和SSE2的指令集中去认识它们。

使用SSE指令注意的问题

(1)SSE指令的内存对齐要求

SSE中大部分指令要求地址是16bytes对齐的,要理解这个问题,以_mm_load_ps函数来解释,这个函数对应于loadps的SSE指令。其原型为:

1
extern __m128 _mm_load_ps(float const*_A);

可以看到,它的输入是一个指向float的指针,返回的就是一个__m128类型的数据,从函数的角度理解,就是把一个float数组的四个元素依次读取,返回一个组合的__m128类型的SSE数据类型,从而可以使用这个返回的结果传递给其它的SSE指令进行运算,比如加法等;从汇编的角度理解,它对应的就是读取内存中连续四个地址的float数据,将其放入SSE新的暂存器中,从而给其他的指令准备好数据进行计算。其使用示例如下:

1
2
float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
__m128 a = _mm_load_ps(input);

这里加载正确的前提是:input这个浮点数阵列都是对齐在16 bytes的边上。否则加载的结果和预期的不一样。如果没有对齐,就需要使用_mm_loadu_ps函数,这个函数用于处理没有对齐在16bytes上的数据,但是其速度会比较慢。

关于内存对齐的问题,这里就不详细讨论什么是内存对齐了,以及如何指定内存对齐方式。这里主要提一下,SSE的intrinsics函数中的扩展的方式:

  • 对于上面的例子,如果要将input指定为16bytes对齐,可以采用的方式是:__declspec(align(16)) float input[4];
  • 为了简化,在xmmintrin.h中定义了一个宏_MM_ALIGN16来表示上面的含义,即:_MM_ALIGN16 float input[4];

(2)大小端问题:

这个只是使用SSE指令的时候要注意一下,我们知道,x86的little-endian特性,位址较低的byte会放在暂存器的右边。也就是说,若以上面的input为例,即

1
2
float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
__m128 a = _mm_load_ps(input);

在载入到XMM暂存器后,暂存器中的 DATA0会是1.0,而DATA1是2.0,DATA2是3.0,DATA3是4.0。如下:

如果需要以相反的顺序载入的话,可以用_mm_loadr_ps 这个intrinsic,根据需要进行选择。

常用的一些SSE指令简介

(1)load系列,用于加载数据(从内存到暂存器),大部分需要16字节对齐

1
2
3
4
5
6
7
8
__m128 _mm_load_ss(float *p) //将一个单精度浮点数加载到寄存器的第一个字节,其它三个字节清零(r0 := *p, r1 := r2 := r3 := 0.0)
__m128 _mm_load_ps(float *p) //将四个单精度浮点数加载到寄存器(r0 := p[0], r1 := p[1], r2 := p[2], r3 := p[3])
__m128 _mm_load1_ps(float *p)//将p地址的值加载到暂存器的四个字节,需要多条指令完成。从性能考虑,在内层循环不要使用这类指令(r0 := r1 := r2 := r3 := *p)

__m128 _mm_loadh_pi(__m128 a, __m64 *p)//
__m128 _mm_loadl_pi(__m128 a, __m64 *p)//
__m128 _mm_loadr_ps(float *p)//以_mm_load_ps反向的顺序加载,需要多条指令完成。(r0 := p[3], r1 := p[2], r2 := p[1], r3 := p[0])
__m128 _mm_loadu_ps(float *p)//_mm_load_ps一样的加载,但是不要求地址是16字节对齐

(2)set系列,用于加载数据,类似于load操作,但是大部分需要多条指令完成,可能不需要16字节对齐

1
2
3
4
5
__m128 _mm_set_ss(float w)//对应于_mm_load_ss的功能,不需要字节对齐,需要多条指令(r0 = w, r1 = r2 = r3 = 0.0)
__m128 _mm_set_ps(float z, float y, float x, float w)//对应于_mm_load_ps的功能,参数是四个单独的单精度浮点数,所以也不需要字节对齐,需要多条指令。(r0=w, r1 = x, r2 = y, r3 = z,注意顺序)
__m128 _mm_set1_ps(float w)//对应于_mm_load1_ps的功能,不需要字节对齐,需要多条指令。(r0 = r1 = r2 = r3 = w)
__m128 _mm_setr_ps(float z, float y, float x, float w)//对应于_mm_loadr_ps功能,不需要字节对齐,需要多条指令。(r0=z, r1 = y, r2 = x, r3 = w,注意顺序)
__m128 _mm_setzero_ps()//清0操作,只需要一条指令。(r0 = r1 = r2 = r3 = 0.0)

(3)store系列,将计算结果等SSE暂存器的数据保存到内存中,与load系列函数的功能对应,基本上都是一个反向的过程。

1
2
3
4
5
6
7
8
void _mm_store_ss(float *p, __m128 a) //一条指令,*p = a0
void _mm_store_ps(float *p, __m128 a) //一条指令,p[i] = a[i]
void _mm_store1_ps(float *p, __m128 a) //多条指令,p[i] = a0
void _mm_storeh_pi(__m64 *p, __m128 a) //
void _mm_storel_pi(__m64 *p, __m128 a) //
void _mm_storer_ps(float *p, __m128 a) //反向,多条指令
void _mm_storeu_ps(float *p, __m128 a) //一条指令,p[i] = a[i],不要求16字节对齐
void _mm_stream_ps(float *p, __m128 a) //直接写入内存,不改变cache的数据

(4)算数指令系列,SSE提供了大量的浮点运算指令,包括加法、减法、乘法、除法、开方、最大值、最小值等等

1
2
__m128 _mm_add_ss (__m128 a, __m128 b)
__m128 _mm_add_ps (__m128 a, __m128 b)

当然算数指令有很多,这里只列举了两个,应该说主要是算术运算指令。

(5)数据类型转换系列

1
2
3
__mm_cvtss_si32 //单精度浮点数转换为有符号32位整数
__mm_cvttss_si32 //单精度浮点数转换为有符号32位整数(带截断操作)
__mm_cvtpi16_ps //16位有符号整数转换为单精度浮点数

SSE指令的加速效果

(1)对于scalar模式的SSE加速

是不是只要采用SSE进行加速就一定会加快运行速度呢?当然不是了,SSE包含packed和scalar两种方式,我们采用scalar运算由于每一次只计算一个值,通过实验对比,使用SSE的scalar加速反而还没有原始的C代码速度快,

(2)对于packed模式的加速

使用packed模式加速,虽然每一次运算4个单精度浮点数,使用SSE优化之后,我们的代码不一定会得到4倍速的提升,因为编译器可能已经自动对某些代码进行SSE优化了。

SSE优化的具体实例

案例说明,比如我要经过两个矩阵的逐元素乘积,我分别通过三种方式来对比

  • 方式一:原生的C/C++代码
  • 方式二:使用SSE的scalar进行优化
  • 方式三:使用OpenCV自带的mul函数

方式一:原生的C/C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//将Mat1和Mat2矩阵元素乘积之后更新到Mat2
void mat_multi(Mat m1, Mat m2)
{
for (int i = 0; i < m1.rows; i++)
{
float * pixel_1 = (float *)m1.data + i * m1.step / 4; //32f
float * pixel_2 = (float *)m2.data + i * m2.step / 4; //32f
for (int j = 0; j < m1.cols; j++)
{
*pixel_2 = (*pixel_1) * (*pixel_2);
pixel_1 += 1;
pixel_2 += 1;
}
}
}

方式二:使用SSE的scalar进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sse_mat_multi(Mat m1, Mat m2)
{
for (int i = 0; i < m1.rows; i++)
{
float * pixel_1 = (float *)m1.data + i * m1.step / 4; //32f
float * pixel_2 = (float *)m2.data + i * m2.step / 4; //32f
for (int j = 0; j < m1.cols; j++)
{
__m128 sse_1 = _mm_load_ps(pixel_1); //将a地址指向的值复制给SSEA
__m128 sse_2 = _mm_load_ps(pixel_2); //将b地址指向的值复制给SSEB
__m128 h = _mm_mul_ss(sse_1, sse_2); //声明了变量并赋值(1.0f)
_mm_storer_ps(pixel_2, h);
pixel_1 += 1;
pixel_2 += 1;
}
}
}

结果测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main(int argv, char *args[])
{
clock_t start, end;
Mat m1 = Mat(Size(10000, 10000), CV_32FC1);
m1.setTo(1);
Mat m2 = Mat(Size(10000, 10000), CV_32FC1);
m1.setTo(2);
start = clock();
mat_multi(m1, m2);
end = clock();
std::cout << "mat multi is : " << (double)(end - start) << std::endl;
start = clock();
sse_mat_multi(m1, m2);
end = clock();
std::cout << "sse mat multi is : " << (double)(end - start) << std::endl;
start = clock();
m1.mul(m2);
end = clock();
std::cout << "opencv mul is : " << (double)(end - start) << std::endl;
getchar();
return 0;
}

/*运行结果为:
mat multi is : 198
sse mat multi is : 259
opencv mul is : 0
*/

结论:由此可见自己写的基于scalar模式下的SSE优化反而变得慢了,而OpenCV原本的矩阵运算非常迅速,速度快的不是一点点,因为现在OpenCV4以上的版本,OpenCV使用了非常多的优化手段,比如parallel,SSE指令集加速,所以我们一般不要自己重写OpenCV已经有了的运算。

SSE优化使用VC++提供的指令集优化对比汇编指令优化

(1)原生态的C/C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CSSETestDlg::ComputeArrayCPlusPlus(
float* pArray1, // [in] first source array
float* pArray2, // [in] second source array
float* pResult, // [out] result array
int nSize) // [in] size of all arrays
{
int i;
float* pSource1 = pArray1;
float* pSource2 = pArray2;
float* pDest = pResult;
for ( i = 0; i < nSize; i++ )
{
*pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2)
* (*pSource2)) + 0.5f;
pSource1++;
pSource2++;
pDest++;
}
}

(2)使用VC++的SSE头文件来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CSSETestDlg::ComputeArrayCPlusPlusSSE(
float* pArray1, // [in] first source array
float* pArray2, // [in] second source array
float* pResult, // [out] result array
int nSize) // [in] size of all arrays
{
int nLoop = nSize/ 4;
__m128 m1, m2, m3, m4;
__m128* pSrc1 = (__m128*) pArray1;
__m128* pSrc2 = (__m128*) pArray2;
__m128* pDest = (__m128*) pResult;
__m128 m0_5 = _mm_set_ps1(0.5f); // m0_5[0, 1, 2, 3] = 0.5
for ( int i = 0; i < nLoop; i++ )
{
m1 = _mm_mul_ps(*pSrc1, *pSrc1); // m1 = *pSrc1 * *pSrc1
m2 = _mm_mul_ps(*pSrc2, *pSrc2); // m2 = *pSrc2 * *pSrc2
m3 = _mm_add_ps(m1, m2); // m3 = m1 + m2
m4 = _mm_sqrt_ps(m3); // m4 = sqrt(m3)
*pDest = _mm_add_ps(m4, m0_5); // *pDest = m4 + 0.5
pSrc1++;
pSrc2++;
pDest++;
}
}

(3)直接使用SSE的汇编指令,将汇编指令嵌入到C/C++里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void CSSETestDlg::ComputeArrayAssemblySSE(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{
int nLoop = nSize/4;
float f = 0.5f;
_asm
{
movss xmm2, f // xmm2[0] = 0.5
shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0]
mov esi, pArray1 // 输入的源数组1的地址送往esi
mov edx, pArray2 // 输入的源数组2的地址送往edx
mov edi, pResult // 输出结果数组的地址保存在edi
mov ecx, nLoop //循环次数送往ecx
start_loop:
movaps xmm0, [esi] // xmm0 = [esi]
mulps xmm0, xmm0 // xmm0 = xmm0 * xmm0
movaps xmm1, [edx] // xmm1 = [edx]
mulps xmm1, xmm1 // xmm1 = xmm1 * xmm1
addps xmm0, xmm1 // xmm0 = xmm0 + xmm1
sqrtps xmm0, xmm0 // xmm0 = sqrt(xmm0)
addps xmm0, xmm2 // xmm0 = xmm1 + xmm2
movaps [edi], xmm0 // [edi] = xmm0
add esi, 16 // esi += 16
add edx, 16 // edx += 16
add edi, 16 // edi += 16
dec ecx // ecx--
jnz start_loop //如果不为0则转向start_loop
}
}

SIMD

First Intrinsic Function Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

#ifdef __AVX__
#include <immintrin.h>
#else
#warning No AVX support - will not compile
#endif

int main(int argc, char **argv)
{
__m256 a = _mm256_set_ps(8.0, 7.0, 6.0, 5.0,
4.0, 3.0, 2.0, 1.0);
__m256 b = _mm256_set_ps(18.0, 17.0, 16.0, 15.0,
14.0, 13.0, 12.0, 11.0);
__m256 c = _mm256_add_ps(a, b);

float d[8];
_mm256_storeu_ps(d, c);

std::cout << "result equals " << d[0] << "," << d[1]
<< "," << d[2] << "," << d[3] << ","
<< d[4] << "," << d[5] << "," << d[6] << ","
<< d[7] << std::endl;

return 0;
}

编译

1
# g++ --std=c++14 -O2 -mavx avx.cpp -o demo

运行

1
2
# ./avx 
result equals 12,14,16,18,20,22,24,26

AVX2 Instruction & Intrinsic Function

本节主要罗列了几种不同功能的指令集和对应的 intrinsic function,没细看的必要,随用随看吧。

Set

Intrinsic Function Operation AVX2 Instruction
_mm256_set1_pd Set all four words with the same value Composite
_mm256_set_pd Set four values Composite
_mm256_setr_pd Set four values, in reverse order Composite
_mm256_setzero_pd Clear all four values VXORPD
_mm256_set_m128d Set lower and higher 128-bit part VINSERTF128

Load

Intrinsic Function Operation AVX2 Instruction
_mm256_load_pd Load four double values, address aligned VMOVAPD ymm, mem
_mm256_loadu_pd Load four double values, address unaligned VMOVUPD ymm, mem
_mm256_maskload_pd Load four double values using mask VMASKMOVPD ymm, mem
_mm256_broadcast_sd Load one double value into all four words VBROADCASTSD ymm, mem
_mm256_broadcast_pd Load a pair of double values into the lower and higher part of vector. VBROADCASTSD ymm, mem
_mm256_i64gather_pd Load double values from memory using indices. VGATHERPD ymm, mem, ymm

Store

Intrinsic Function Operation AVX2 Instruction
_mm256_store_pd Store four values, address aligned VMOVAPD
_mm256_storeu_pd Store four values, address unaligned VMOVUPD
_mm256_maskstore_pd Store four values using mask VMASKMOVPD
_mm256_storeu2_m128d Store lower and higher 128-bit parts into different memory locations Composite
_mm256_stream_pd Store values without caching, address aligned VMOVNTPD

Math

Intrinsic Function Operation AVX2 Instruction
_mm256_add_ps Addition VADDPS
_mm256_sub_ps Subtraction VSUBPS
_mm256_addsub_ps Alternatively add and subtract VADDSUBPS
_mm256_hadd_ps Half addition VHADDPS
_mm256_hsub_pd Half subtraction VHSUBPD
_mm256_mul_pd Multiplication VMULPD
_mm256_sqrt_pd Squared Root VSQRTPD
_mm256_max_pd Computes Maximum VMAXPD
_mm256_min_pd Computes Minimum VMINPD
_mm256_ceil_pd Computes Ceil VROUNDPD
_mm256_floor_pd Computes Floor VROUNDPD
_mm256_round_pd Round VROUNDPD
_mm256_dp_ps Single precision dot product VDPPS
_mm256_fmadd_pd Fused multiply-add VFMADD132pd
_mm256_fmsub_pd Fused multiply-subtract VFMSUB132pd
_mm256_fmaddsub_pd Alternatively fmadd, fmsub VFMADDSUB132pd

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
// n a multiple of 4, x is 32-byte aligned
void addindex_vec2(double *x, int n) {
__m256d x_vec, init, incr, ind;
ind = _mm256_set_pd(3, 2, 1, 0);
incr = _mm256_set1_pd(4);
for (int i = 0; i < n; i+=4) {
x_vec = _mm256_load_pd(x+i); // load 4 doubles
x_vec = _mm256_add_pd(x_vec, ind); // add the two
ind = _mm256_add_pd(ind, incr); // update ind
_mm256_store_pd(x+i, x_vec); // store back
}
}

Compare

Intrinsic Function & Instruction Macro For Operation Operation
_mm256_cmp_pd / VCMPPD _CMP_EQ_OQ Equal
_CMP_EQ_UQ Equal (unordered)
_CMP_GE_OQ Greater Than or Equal
_CMP_GT_OQ Greater Than
_CMP_LE_OQ Less Than or Equal
_CMP_LT_OQ Less Than
_CMP_NEQ_OQ Not Equal
_CMP_NEQ_UQ Not Equal (unordered)
_CMP_NGE_UQ Not Greater Than or Equal (unordered)
_CMP_NGT_UQ Not Greater Than (unordered)
_CMP_NLE_UQ Not Less Than or Equal (unordered)
_CMP_NLT_UQ Not Less Than (unordered)
_CMP_TRUE_UQ True (unordered)
_CMP_FALSE_OQ False
_CMP_ORD_Q Ordered
_CMP_UNORD_Q Unordered

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <xmmintrin.h>
void fcond_vec1(double *x, size_t n) {
int i;
__m256d vt, vmask, vp, vm, vr, ones, mones, thresholds;
ones = _mm256_set1_pd(1.);
mones = _mm256_set1_pd(-1.);
thresholds = _mm256_set1_pd(0.5);
for(i = 0; i < n; i+=4) {
vt = _mm256_load_pd(x+i);
vmask = _mm256_cmp_pd(vt, thresholds, _CMP_GT_OQ);
vp = _mm256_and_pd(vmask, ones);
vm = _mm256_andnot_pd(vmask, mones);
vr = _mm256_add_pd(vt, _mm256_or_pd(vp, vm));
_mm256_store_pd(x+i, vr);
}
}

Convert

Intrinsic Function Operation AVX2 Instruction
_mm256_cvtepi32_pd Convert from 32-bit integer VCVTDQ2PD
_mm256_cvtepi32_ps Convert from 32-bit integer VCVTDQ2PD
_mm256_cvtpd_epi32 Convert to 32-bit integer VCVTPD2DQ
_mm256_cvtps_epi32 Convert to 32-bit integer VCVTPS2DQ
_mm256_cvtps_pd Convert from floats VCVTPS2PD
_mm256_cvtpd_ps Convert to floats VCVTPD2PS
_mm256_cvttpd_epi32 Convert to 32-bit integer with truncation VCVTPD2DQ
_mm256_cvtsd_f64 Extract MOVSD
_mm256_cvtss_f32 Extract MOVSS

img

Shuffles

Intrinsic Function Operation AVX2 Instruction
_mm256_unpackhi_pd Unpack High VUNPCKHPD
_mm256_unpacklo_pd Unpack Low VUNPCKLPD
_mm256_movemask_pd Create four-bit mask VMOVMSKPD
_mm256_movedup_pd Duplicates VMOVDDUP
_mm256_blend_pd Selects data from 2 sources using constant mask VBLENDPD
_mm256_blendv_pd Selects data from 2 sources using variable mask VBLENDVPD
_mm256_insertf128_pd Insert 128-bit value into packed array elements selected by index. VINSERTF128
_mm256_extractf128_pd Extract 128-bits selected by index. VEXTRACTF128
_mm256_shuffle_pd Shuffle VSHUFPD
_mm256_permute_pd Permute VPERMILPD
_mm256_permute4x64_pd Permute 64-bits elements VPERMPD
_mm256_permute2f128_pd Permute 128-bits elements VPERM2F128

img

img

示例代码

1
2
3
4
5
6
7
8
9
void fcond(double *x, size_t n) {
int i;
for(i = 0; i < n; i++) {
if(x[i] > 0.5)
x[i] += 1.;
else
x[i] -= 1.;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <immintrin.h>
void fcond_vec2(double *x, size_t n) {
int i;
__m256d vt, vmask, vp, vm, vr, ones, mones, thresholds;
ones = _mm256_set1_pd(1.);
mones = _mm256_set1_pd(-1.);
thresholds = _mm256_set1_pd(0.5);
for(i = 0; i < n; i+=4) {
vt = _mm256_load_pd(x+i);
vmask = _mm256_cmp_pd(vt, thresholds, _CMP_GT_OQ);
vb = _mm256_blendv_pd(mones, ones, vmask);
vr = _mm256_add_pd(vt, vb);
_mm256_store_pd(x+i, vr);
}
}

AVX2 Samples

上一节中,罗列了一堆无聊的 AVX2 指令和对应的 Intrinsic Function,下面我们通过一些具体的例子来演示如何使用Intrinsic Function进行编程。

Gelu

Gelu 是一类激活算子,其函数定义如下:

img

注意,Gelu 中包含了一个三角函数 tanh 操作,但是如果不使用Intel c++ compiler,编译器可能不支持相应的 AVX2 指令;因此,为了完成 tanh 和其他的科学计算函数如三角函数、指数等操作,可以使用两种方式来解决

  1. 直接编写科学计算函数的近似实现,平滑替换;
  2. 使用 https://github.com/shibatch/sleef 或者 https://github.com/vectorclass/version2 提供的高效实现;

方法 1 在用法上较为方便,省去了学习第三方库的时间成本,但是本质上是一种重复造轮子的行为;方法2 中介绍的两个库也算轻量,并不存在陡峭的学习曲线,十分推荐;不过在本文中,主要目的是介绍 intrinsic function 的使用,所以尽量不调用第三方库。

Tanh 函数的近似实现为

1
2
3
4
5
6
float fast_tanh(float x) {
float x2 = x * x;
float a = x * (135135.0f + x2 * (17325.0f + x2 * (378.0f + x2)));
float b = 135135.0f + x2 * (62370.0f + x2 * (3150.0f + x2 * 28.0f));
return a / b;
}

相应的实现为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void _AVX_Gelu(float* dst, const float* src, size_t size) {
auto var1 = _mm256_set1_ps(0.044715f);
auto var2 = _mm256_set1_ps(0.79788458f);
auto var3 = _mm256_set1_ps(378.f);
auto var4 = _mm256_set1_ps(17325.f);
auto var5 = _mm256_set1_ps(135135.f);
auto var6 = _mm256_set1_ps(28.f);
auto var7 = _mm256_set1_ps(3150.f);
auto var8 = _mm256_set1_ps(62370.f);
auto var9 = _mm256_set1_ps(135135.f);
auto var10 = _mm256_set1_ps(0.5);
auto varOne = _mm256_set1_ps(1.f);
auto varNegOne = _mm256_set1_ps(-1.f);

for (int i = 0; i < size; i++) {
// 计算 x^3
auto x = _mm256_loadu_ps(src + i * 8);
auto y = _mm256_mul_ps(x, x);
y = _mm256_mul_ps(y, x);
// 计算 0.044715 * x^3
y = _mm256_mul_ps(y, var1);
// 计算 0.044715 * x^3 + x
y = _mm256_add_ps(y, x);
// 计算 sqrt(2 / PI) * (0.044715 * x^3 + x)
y = _mm256_mul_ps(y, var2);

// y = tanh(y)
{
auto y2 = _mm256_mul_ps(y, y);
auto w = _mm256_add_ps(y2, var3);
w = _mm256_mul_ps(w, y2);
w = _mm256_add_ps(w, var4);
w = _mm256_mul_ps(w, y2);
w = _mm256_add_ps(w, var5);
w = _mm256_mul_ps(w, y);
auto z = _mm256_mul_ps(y2, var6);
z = _mm256_add_ps(z, var7);
z = _mm256_mul_ps(z, y2);
z = _mm256_add_ps(z, var8);
z = _mm256_mul_ps(z, y2);
z = _mm256_add_ps(z, var9);
z = _mm256_div_ps(w, z);
z = _mm256_max_ps(z, varNegOne);
y = _mm256_min_ps(z, varOne);
}

y = _mm256_add_ps(y, varOne);
y = _mm256_mul_ps(y, x);
y = _mm256_mul_ps(y, var10);
_mm256_storeu_ps(dst + i * 8, y);
}
}

代码都十分直白,不言自明。

MatrixAdd

下面的例子演示了如何使用 intrinsic function 做 col-major 矩阵的加法运算,由于 AVX2 指令集可以一次打包 8 个浮点数运算,所以代码中将 PACK_UNIT 设置为 8。

代码的逻辑十分简单,按行循环遍历,按列打包浮点数运算。出于方便考虑,默认代码中 cols 是8的倍数,可以省去尾数处理的逻辑。显而易见,该实现没有考虑矩阵的规模进行针对性优化,比如运算矩阵和结果矩阵有多大,直接存取会发生多少次cache miss?如何进行循环展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define PACK_UNIT 8

void MatrixAdd(float* C, const float* A, const float* B, const size_t cs,
const size_t as, const size_t bs, const size_t rows,
const size_t cols) {
for (int row = 0; row < rows; ++row) {
auto a = A + as * row;
auto b = B + bs * row;
auto c = C + cs * row;

for (int col = 0; col < cols; col += PACK_UNIT) {
_mm256_storeu_ps(c + PACK_UNIT * col,
_mm256_add_ps(_mm256_loadu_ps(b + PACK_UNIT * col),
_mm256_loadu_ps(a + PACK_UNIT * col)));
}
}
}

编译

1
g++ --std=c++14 -O2 -mavx2 matrixadd.cc -o madd

MatrixTranspose

以下代码用以演示如何使用 intrinsic function 进行 8 x 8 矩阵的转换,重点在于理解 mm256_unpacklo_ps 、mm256_unpackhi_ps 和 __mm256_shuffle_ps 指令的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void matrixTranspose(float* dst, const float* src) {
__m256 r0, r1, r2, r3, r4, r5, r6, r7;
__m256 t0, t1, t2, t3, t4, t5, t6, t7;

r0 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[0 * 8 + 0])),
_mm_load_ps(&src[4 * 8 + 0]), 1);
r1 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[1 * 8 + 0])),
_mm_load_ps(&src[5 * 8 + 0]), 1);
r2 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[2 * 8 + 0])),
_mm_load_ps(&src[6 * 8 + 0]), 1);
r3 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[3 * 8 + 0])),
_mm_load_ps(&src[7 * 8 + 0]), 1);
r4 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[0 * 8 + 4])),
_mm_load_ps(&src[4 * 8 + 4]), 1);
r5 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[1 * 8 + 4])),
_mm_load_ps(&src[5 * 8 + 4]), 1);
r6 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[2 * 8 + 4])),
_mm_load_ps(&src[6 * 8 + 4]), 1);
r7 =_mm256_insertf128_ps(_mm256_castps128_ps256(_mm_load_ps(&src[3 * 8 + 4])),
_mm_load_ps(&src[7 * 8 + 4]), 1);

t0 = _mm256_unpacklo_ps(r0, r1);
t1 = _mm256_unpackhi_ps(r0, r1);
t2 = _mm256_unpacklo_ps(r2, r3);
t3 = _mm256_unpackhi_ps(r2, r3);
t4 = _mm256_unpacklo_ps(r4, r5);
t5 = _mm256_unpackhi_ps(r4, r5);
t6 = _mm256_unpacklo_ps(r6, r7);
t7 = _mm256_unpackhi_ps(r6, r7);

r0 = _mm256_shuffle_ps(t0, t2, 0x44);
r1 = _mm256_shuffle_ps(t0, t2, 0xEE);
r2 = _mm256_shuffle_ps(t1, t3, 0x44);
r3 = _mm256_shuffle_ps(t1, t3, 0xEE);
r4 = _mm256_shuffle_ps(t4, t6, 0x44);
r5 = _mm256_shuffle_ps(t4, t6, 0xEE);
r6 = _mm256_shuffle_ps(t5, t7, 0x44);
r7 = _mm256_shuffle_ps(t5, t7, 0xEE);

_mm256_store_ps(&dst[0 * 8], r0);
_mm256_store_ps(&dst[1 * 8], r1);
_mm256_store_ps(&dst[2 * 8], r2);
_mm256_store_ps(&dst[3 * 8], r3);
_mm256_store_ps(&dst[4 * 8], r4);
_mm256_store_ps(&dst[5 * 8], r5);
_mm256_store_ps(&dst[6 * 8], r6);
_mm256_store_ps(&dst[7 * 8], r7);
}

Softmax

softmax 的函数方程并不复杂,实现时关键点在于如何实现 exp ,下面的实现中参考了 Fastest Implementation of Exponential Function Using AVX ,可以一起研究下。

img

关于如何快速计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
void _AVX_Softmax(float* dest, const float* source, size_t size) {
float tmpfloat8[8];
int count = size / 8;

// step 1: get maxValue
float maxValue = source[0];
if (count > 0) {
auto maxVal = _mm256_loadu_ps(source);
for (int i = 1; i < count; i++) {
maxVal = _mm256_max_ps(maxVal, _mm256_loadu_ps(source + i * 8));
}
_mm256_storeu_ps(tmpfloat8, maxVal);
maxValue = tmpfloat8[0] > tmpfloat8[1] ? tmpfloat8[0] : tmpfloat8[1];
for (int i = 2; i < 8; i++) {
maxValue = maxValue > tmpfloat8[i] ? maxValue : tmpfloat8[i];
}
}

// step 2: get exp(x - maxValue) and sum(exp(x - maxValue))
float sumValue = 0.f;
if (count > 0) {
auto sumVal = _mm256_set1_ps(0.f);
auto p0 = _mm256_set1_ps(0.6931471805599453);
auto p1 = _mm256_set1_ps(1.4426950408889634);
auto p2 = _mm256_set1_ps(1.f);
auto p3 = _mm256_set1_ps(1.f);
auto p4 = _mm256_set1_ps(0.5);
auto p5 = _mm256_set1_ps(0.1666666666666666);
auto p6 = _mm256_set1_ps(0.041666666666666664);
auto p7 = _mm256_set1_ps(0.008333333333333333);
auto xMax = _mm256_set1_ps(87);
auto xMin = _mm256_set1_ps(-87);
auto basic = _mm256_set1_epi32(1 << 23);
auto temp127 = _mm256_set1_epi32(127);

for (int i = 0; i < count; ++i) {
auto x = _mm256_sub_ps(_mm256_loadu_ps(source + i * 8),
_mm256_set1_ps(maxValue));
x = _mm256_max_ps(x, xMin);
x = _mm256_min_ps(x, xMax);
auto div = _mm256_mul_ps(x, p1);
auto divInt = _mm256_cvtps_epi32(div);
div = _mm256_cvtepi32_ps(divInt);
auto div2 = _mm256_add_epi32(divInt, temp127);
div2 = _mm256_mullo_epi32(div2, basic);
auto expBasic = _mm256_castsi256_ps(div2);
auto xReamin = _mm256_sub_ps(x, _mm256_mul_ps(div, p0));
auto t = xReamin;
auto c0 = _mm256_mul_ps(p7, t);
auto c1 = _mm256_add_ps(c0, p6);
auto c2 = _mm256_mul_ps(c1, t);
auto c3 = _mm256_add_ps(c2, p5);
auto c4 = _mm256_mul_ps(c3, t);
auto c5 = _mm256_add_ps(c4, p4);
auto c6 = _mm256_mul_ps(c5, t);
auto c7 = _mm256_add_ps(c6, p3);
auto c8 = _mm256_mul_ps(c7, t);
auto c9 = _mm256_add_ps(c8, p2);
auto expRemain = c9;
auto expRes = _mm256_mul_ps(expBasic, expRemain);
sumVal = _mm256_add_ps(expRes, sumVal);
_mm256_storeu_ps(dest + 8 * i, expRes);
}
_mm256_storeu_ps(tmpfloat8, sumVal);
for (int i = 0; i < 8; i++) {
sumValue += tmpfloat8[i];
}
}

auto param = 0.6931471805599453;
float xLimit = 87;

// step 3: get x / sum and store
for (int i = 0; i < count; ++i) {
// using 1 / ((1 / x) * sum) instead x * (1 / sum) or x / sum for some bugs
// in intel cpu
auto x = _mm256_rcp_ps(_mm256_loadu_ps(dest + 8 * i));
auto y = _mm256_set1_ps(sumValue);
auto z = _mm256_rcp_ps(_mm256_mul_ps(x, y));
_mm256_storeu_ps(dest + 8 * i, z);
}
}

本文介绍的内容和示例终究是小打小闹,真正有价值的工作还是在GEMM和Conv 这类计算密集型的热点算子上做出深度优化。后面有计划再介绍vectorclass 和 xbyak ,然后通过深度学习推理框架中GEMM算子为例来演示如何进行分块、打包、寄存器优化等技术。

在多核的平台上开发并行化的程序,必须合理地利用系统的资源 - 如与内核数目相匹配的线程,内存的合理访问次序,最大化重用缓存。有时候用户使用(系统)低级的应用接口创建、管理线程,很难保证是否程序处于最佳状态。

Intel Thread Building Blocks (TBB) 很好地解决了上述问题:

  • TBB提供C++模版库,用户不必关注线程,而专注任务本身。
  • 抽象层仅需很少的接口代码,性能上毫不逊色。
  • 灵活地适合不同的多核平台。
  • 线程库的接口适合于跨平台的移植(Linux, Windows, Mac)
  • 支持的C++编译器 – Microsoft, GNU and Intel

主要的功能:

  • 通用的并行算法
    • 循环的并行:
      • parallel_for, parallel_reduce – 相对独立的循环层
      • parallel_scan – 依赖于上一层的结果
  • 流的并行算法
    • parallel_while – 用于非结构化的流或堆
    • pipeline - 对流水线的每一阶段并行,有效使用缓存
  • 并行排序
    • parallel_sort – 并行快速排序,调用了parallel_for
  • 任务调度者
    • 管理线程池,及隐藏本地线程复杂度
    • 并行算法的实现由任务调度者的接口完成
    • 任务调度者的设计考虑到本地线程的并行所引起的性能问题
  • 并行容器
    • concurrent_hash_map
    • concurrent_vector
    • concurrent_queue
  • 同步原语
    • atomic
    • mutex
    • spin_mutex – 适合于较小的敏感区域
    • queuing_mutex – 线程按次序等待(获得)一个锁
    • spin_rw_mutex
    • queuing_rw_mutex
  • 高性能的内存申请
    • 使用TBB的allocator 代替 C语言的 malloc/realloc/free 调用
    • 使用TBB的allocator 代替 C++语言的 new/delete 操作

术语与基本概念

分割(splitable concept):

包含一个分割构造函数的类型是可分割的。分割构造函数原型为:

1
X::X(X& obj, Split)

能将实例obj分割为obj以及一个新构造的对象。其中的Split是一个哑元参数,在tbb_stddef.h中的有其定义(一个空类):

1
2
3
class split {

};

TBB将在以下情况使用分割构造:

  • 将一个区域(range)分为两个子区域(subrange)以便并行处理
  • 将一个主体(body,即函数对象)分为两个主体以便并行处理

区域(range concept)

描述了一种集合类型的需求,这种集合可被递归分割。区域类型R必须满足以下需求:

  • R::R(const R& ):构造函数
  • R::~R():析构函数
  • bool R::empty() const:区域为空返回ture
  • bool R::is_divisible() const:如果区域可再分,返回ture
  • R::R(R& r, split):将r分为两个子区域

TBB内置了三种区域模板:

1
2
3
4
5
6
7
8
template<typenameValue>
class blocked_range;

template<typenameRowValue, typename ColValue>
class blocked_range2d;

template<typenamePageValue, typename RowValue, typename ColValue>
class blocked_range3d;

blocked_range<Value>描述了一个能被递归分割的半开放区域[I,j)。

分割器(partitioner):

指定了循环模板将其任务分割后分配给各个线程的方式。循环模板(如parallel_for、parallel_reduce、parallel_scan)的默认行为只是尽量递归将区域分割以使所有的处理器处于繁忙状态,不一定分割的尽可能合适。如下表所示,可选的分割器参数允许指定其他的行为:

  • const auto_partitioner&:按负载平衡进行分割,而不是真正依照Range::is_divisible的许可。当与类(比如blocked_range)一起使用时,选择一个合适的粒度也很重要。常规可接受的性能可以通过尺寸为1的默认粒度来达到。
  • affinity_partitioner&:与auto_partitioner类似,但通过选择映射子区域到工作线程提高缓存的亲缘性。当一个循环体在一个相同的数据集再次执行并且该数据集与缓存相符时,能显著提高性能。
  • const simple_partitioner&:递归分割一个区域,直到不能再分。何时终止递归分割由函数Range::is_devisible完全决定。当与blocked_range等类一起使用时,选择合适的可并发粒度在限制开销方面至关重要。

基本算法参考及使用

基本算法(algorithms)

Intel TBB提供的大多数并行算法支持泛型。但是这些受支持的类型必须实现必要的概念方法。并行算法可以嵌套,例如,一个parallel_for的内部可以调用另一个parallel_for。目前版本的TBB(4.0)提供的基本算法如下所示:

  • parallel_for
  • parallel_reduce
  • parallel_scan
  • parallel_do
  • 管道(pipeline、parallel_pipeline)
  • parallel_sort
  • parallel_invoke

parallel_for

parallel_for是在一个值域执行并行迭代操作的模板函数。

1
2
3
4
5
6
7
8
9
10
11
12
template<typenameIndex, typename Func>
Func parallel_for( Index first, Index_type last, const Func& f
[, task_group_context&group] );

template<typenameIndex, typename Func>
Func parallel_for( Index first, Index_type last,
Index step, const Func&f
[, task_group_context&group] );

template<typenameRange, typename Body>
void parallel_for( const Range& range, const Body& body,
[, partitioner[,task_group_context& group]] );

头文件

1
#include “tbb/parallel_for.h”

parallel_for(first, last,step, f)表示一个循环的并行执行:

1
for(auto i= first; i<last; i+=step) f(i);

注意以下几点:

  1. 索引类型必须是整形
  2. 循环不能回环
  3. 步长(step)必须为正,如果省略了,隐指为1
  4. 并没有保证迭代操作以并行方式进行
  5. 较小的迭代等待更大的迭代可能会发生死锁
  6. 分割策略总是auto_partitioner

parallel_for(range, body, partitioner)提供了并行迭代的泛型形式。它表示在区域的每个值,并行执行bodypartitioner选项指定了分割策略。Range类型必须符合Range概念模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>
#include <tbb/tbb.h>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>

using namespace std;
using namespace tbb;

typedef vector<int>::iterator IntVecIt;

struct body
{
void operator()(const blocked_range<IntVecIt>&r)const
{
for(auto i = r.begin(); i!=r.end(); i++)
cout<<*i<<' ';
}
};

int main()
{
vector<int> vec;
for(int i=0; i<10; i++)
vec.push_back(i);

parallel_for(blocked_range< IntVecIt>(vec.begin(), vec.end())
, body());
return 0;
}

parallel_reduce

parallel_reduce模板在一个区域迭代,将由各个任务计算得到的部分结果合并,得到最终结果。

parallel_reduce对区域(range)类型的要求与parallel_for一样。body类型需要分割构造函数以及一个join方法。body的分割构造函数拷贝运行循环体需要的只读数据,并分配并归操作中初始化并归变量的标志元素。join方法会组合并归操作中各任务的结果。

1
2
3
4
5
6
7
8
9
template<typenameRange, typename Value, 
typename Func, typename Reduction>
Value parallel_reduce(const Range& range, const Value& identity,
const Func& func,const Reduction& reduction,
[, partitioner[,task_group_context& group]] );

template<typenameRange, typename Body>
void parallel_reduce(const Range& range, const Body& body
[, partitioner[,task_group_context& group]] );

头文件

1
#include “tbb/parallel_reduce.h”

parallel_reduce模板有两种形式。函数形式是为方便与lambda表达式一起使用而设计。第二种形式是为了最小化数据拷贝。下面总结了第一种形式中的identity,func,reduction的类型要求要求:

  • Value IdentityFunc::operator()的左标识元素
  • Value Func::operator()(const Range& range, const Value& x):累计从初始值x开始的子区域的结果
  • Value Reduction::operator()(const Value& x, const Value& y);:合并x跟y的结果

parallel_reduce使用分割构造函数来为每个线程生成一个或多个body的拷贝。当它拷贝body的时候,也许body的operator()或者join()正在并发运行。要确保这种并发运行下的安全。典型应用中,这种安全要求不会消耗你太多的精力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <tbb/parallel_reduce.h>
#include <tbb/blocked_range.h>
#include <vector>

using namespace std;
using namespace tbb;

int main()
{
vector<int> vec;
for(int i=0; i<100; i++)
vec.push_back(i);

int result = parallel_reduce(blocked_range<vector<int>::iterator>(vec.begin(), vec.end()),
0,
[](const blocked_range<vector<int>::iterator>& r, int init)->int{

for(auto a = r.begin(); a!=r.end(); a++)
init+=*a;
return init;
},

[](int x, int y)->int{
return x+y;
}
);

cout<<"result:"<<result<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
size_t n = 1<<26;
float res = tbb::parallel_reduce(tbb::blocked_range<size_t>(0, n), (float)0,
[&] (tbb::blocked_range<size_t> r, float local_res) {
for (size_t i = r.begin(); i < r.end(); i++) {
local_res += std::sin(i);
}
return local_res;
}, [] (float x, float y) {
return x + y;
});

std::cout << res << std::endl;
return 0;
}

parallel_scan

并行计算前束(prefix)的函数模板。即输入一个数组,生成一个数组,其中每个元素的值都是原数组中在此元素之前的元素的某个运算符的结果的累积。比如求和:输入:[2, 8, 9, -4, 1, 3, -2, 7],生成:[0, 2, 10, 19, 15, 16, 19, 17]

1
2
3
4
5
6
7
8
9
10
template<typename Range, typename Body> 
void parallel_scan( const Range& range, Body& body );

template<typename Range, typename Body>
void parallel_scan( const Range& range, Body& body, const
auto_partitioner& );

template<typename Range, typename Body>
void parallel_scan( const Range& range, Body& body, const
simple_partitioner& );
1
#include “tbb/parallel_scan.h”

parallel_scan<Range,Body>以泛型形式实现并行前束。它的要求如下:

  • void Body::operator()(const Range& r, pre_scan tag):累积归纳区域r
  • ·void Body::operator()(const Range& r, final_scan tag)`:归纳区域r以及计算扫描结果
  • Body::Body(Body& b, split):分割b以便this和b能被单独累积归纳。*this对象即本表下行的对象a
  • void Body::reverse_join(Body& a):将a的归纳结果合并到this,this是先前从a的分割构造函数中创建的。*this对象即本表上一行中的对象b
  • void Body::assign(Body& b):将b的归纳结果赋给this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <tbb/parallel_scan.h>
#include <tbb/blocked_range.h>
#include <iostream>
using namespace tbb;
using namespace std;

template<typename T>
class Body
{
T _sum;
T* const _y;
const T* const _x;
public:
Body(T y[], const T x[]):_sum(0), _x(x), _y(y){}
T get_sum() const
{
return _sum;
}

template<typename Tag>
void operator()(const blocked_range<int>& r, Tag)
{
T temp = _sum;
for(int i = r.begin(); i< r.end(); i++)
{
temp+=_x[i];
if(Tag::is_final_scan())
_y[i] = temp;
}

_sum = temp;
}

Body(Body&b, split):_x(b._x), _y(b._y), _sum(0){}
void reverse_join(Body& a)
{
_sum+=a._sum;
}
void assign(Body& b)
{
_sum = b._sum;
}
};

int main()
{
int x[10] = {0,1,2,3,4,5,6,7,8,9};
int y[10];
Body<int> body(y,x);
parallel_scan(blocked_range<int>(0, 10), body);
cout<<"sum:"<<body.get_sum()<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
size_t n = 1<<26;
std::vector<float> a(n);
float res = tbb::parallel_scan(tbb::blocked_range<size_t>(0, n), (float)0,
[&] (tbb::blocked_range<size_t> r, float local_res, auto is_final) {
for (size_t i = r.begin(); i < r.end(); i++) {
local_res += std::sin(i);
if (is_final) {
a[i] = local_res;
}
}
return local_res;
}, [] (float x, float y) {
return x + y;
});

std::cout << a[n / 2] << std::endl;
std::cout << res << std::endl;
return 0;
}

parallel_do

并行处理工作项的模板函数

1
2
3
4
5
template<typename InputIterator, typename Body>
void parallel_do( InputIterator first, InputIteratorlast,
Body body[,task_group_context& group] );

#include "tbb/parallel_do.h"

parallel_do(first, last,body)在对处于半开放区间[first, last)的元素应用函数对象body(不见得并行运行)。如果body重载的()函数的第二个参数(类型为parallel_do_feeder)不为空,那么可以增加另外的工作项。当对输入队列或者通过parallel_do_feeder::add方法添加的所有项x执行的body(x)都返回后,函数结束。其中的parallel_do_feeder允许parallel_dobody添加额外的工作项,只有parallel_do才能创建或者销毁parallel_do_feeder对象。其他的代码对parallel_do_feeder唯一能做的事就是调用它的add方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <tbb/parallel_do.h>
#include <iostream>
#include <vector>
using namespace std;
using namespace tbb;

struct t_test
{
string msg;
int ref;
void operator()()const
{
cout<<msg<<endl;
}
};

template <typename T>
struct body_test
{
void operator()(T* t, parallel_do_feeder<T*>& feeder) const
{
(*t)();
if(t->ref == 0)
{
t->msg = "added msg";
feeder.add(t);
t->ref++;
}
}
};

int main()
{
t_test *pt = new t_test;
pt->ref = 0;
pt->msg = "original msg";

vector<t_test*> vec;
vec.push_back(pt);
parallel_do(vec.begin(), vec.end(), body_test<t_test>());
delete pt;
return 0;
}

pipleline

1
2
3
4
5
6
7
8
9
10
class pipeline
{
public:
pipeline();
~pipeline();
void add_filter( filter& f );
void run( size_t max_number_of_live_tokens
[,task_group_context& group] );
void clear();
};

可按以下步骤使用pipeline类:

  1. 从filter继承类f,f的构造函数传递给基类filter的构造函数一个参数,来指定它的模式
  2. 重载虚方法filter::operator()来实现过滤器对元素处理,并返回一个将被下一个过滤器处理的元素指针。如果流里没有其他的要处理的元素,返回空值。最后一个过滤器的返回值将被忽略。
  3. 生成pipeline类的实例
  4. 生成过滤器f的实例,并将它们按先后顺序加给pipeline。一个过滤器的实例一次只能加给一个pipeline。同一时间,一个过滤器禁止成为多个pipeline的成员。
  5. 调用pipeline::run方法。参数max_number_of_live_tokens指定了能并发运行的阶段数量上限。较高的值会以更多的内存消耗为代价来增加并发性。

函数parallel_pipeline提供了一种强类型的面向lambda的方式来建立并运行管道。

过滤器基类filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class filter
{
public:
enum mode
{
parallel = implementation-defined,
serial_in_order = implementation-defined,
serial_out_of_order =implementation-defined
};

bool is_serial() const;
bool is_ordered() const;
virtual void* operator()( void* item ) = 0;
virtual void finalize( void* item ) {}
virtual ~filter();
protected:
filter( mode );

};

过滤器模式有三种模式:parallel,serial_in_order,serial_out_of_order

  • parallel过滤器能不按特定的顺序并行处理多个工作项
  • serial_out_of_order过滤器不按特定的顺序每次处理一个工作项
  • serial_in_order过滤器每次处理一个工作项。管道中的所有serial_in_order过滤器都按同样的顺序处理工作项。

由于parallel过滤器支持并行加速,所以推荐使用。如果必须使用serial过滤器,那么serial_out_of_order类型的过滤器是优先考虑的,因为他在处理顺序上的约束较少。

线程绑定过滤器thread_bound_filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class thread_bound_filter: public filter
{
protected:
thread_bound_filter(mode filter_mode);
public:
enum result_type
{
success,
item_not_available,
end_of_stream
};
result_type try_process_item();
result_type process_item();
};

管道中过滤器的抽象基类,线程必须显式为其提供服务。当一个过滤器必须由某个指定线程执行的时候会派上用场。服务于thread_bound_filter的线程不能是调用pipeline::run()的线程。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<iostream>
#include <tbb/pipeline.h>
#include<tbb/compat/thread>
#include<tbb/task_scheduler_init.h>

using namespacestd;
using namespacetbb;
char input[] ="abcdefg\n";

class inputfilter:public filter
{
char *_ptr;
public:
void *operator()(void *)
{
if(*_ptr)
{
cout<<"input:"<<*_ptr<<endl;
return _ptr++;
}
else
return 0;
}
inputfilter():filter(serial_in_order),_ptr(input){}
};

class outputfilter: public thread_bound_filter
{
public:
void *operator()(void *item)
{
cout<<*(char*)item;
return 0;
}
outputfilter():thread_bound_filter(serial_in_order){}
};

void run_pipeline(pipeline *p)
{
p->run(8);
}

int main()
{
inputfilter inf;
outputfilter ouf;
pipeline p;
p.add_filter(inf);
p.add_filter(ouf);
//由于主线程服务于继承自thread_bound_filter的outputfilter,所以pipeline要运行在另一个单独的线程
thread t(run_pipeline, &p);
while(ouf.process_item()!=thread_bound_filter::end_of_stream)
continue;
t.join();
return 0;
}

简单循环的并行化

假设你想要对某个数组的所有元素都应用函数 Foo,并且能安全地同时处理。先列出来串行化的代码版本:

1
2
3
4
5
void SerialApplyFoo( float a[], size_t n )
{
for( size_t i=0; i!=n; ++i )
Foo(a[i]);
}

迭代空间的类型为 size_t ,范围从0到 n-1 。模板函数tbb::parallel_for会将此迭代空间打散为一些块(chunk),在每个块上运行一个独立的线程。将此循环并行化的第一个步骤是将此循环体转变成对块的操作的形式。这种形式是一种STL风格的函数对象,叫做实体对象(body object),其中 operator() 处理一个块。下面的代码声明了这个实体对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "tbb/tbb.h"
using namespace tbb;

class ApplyFoo
{
float *const my_a;
public:
void operator ()(const blocked_range<size_t>& r) const
{
float *a = my_a;
for (size_t i = r.begin(); i != r.end(); ++i) Foo(a[i]);
}
ApplyFoo(float a[]) : my_a(a)
{
}
};

例子中的 using 指令可以使你在使用 tbb 中定义的数据时不需要每次都加上 tbb 前缀。后面的例子都假定提供了这么个 using 指令。

注意operator()的参数。blocked_range<T>是intel tbb 库提供的一个模板类。它以类型T上声明了一个一维迭代空间。parallel_for也能接受其他类型的迭代空间。Intel TBB 库为二维空间提供了blocked_range2d

ApplyFoo 的实例需要成员变量来记住所有在初始循环的外部定义却在内部使用的局部变量。由于parallel_for 并不在意实体对象的创建方式,这些成员变量通常由实体对象的构造函数初始化。模板函数parallel_for 要求实体对象有拷贝构造函数,通过调用它为每个工作者线程创建隔离的拷贝。它也通过调用析构函数来销毁这些拷贝。在大多数情况下,隐式产生的拷贝构造函数与析构函数就够用了。如果不满足需求,那么为了一致性,你就要同时定义两者。

因为实体对象可能被拷贝,它的operator()就不能修改实体。否则,这些改动对于调用parallel_for的线程可见与否依赖于operator()执行是在原始对象还是在拷贝对象上。为了凸显这点小差别,parallel_for要求实体对象的operator ()声明为 const.

示例的operator()my_a加载到局部变量a

一旦你将循环体写成了实体对象,使用下面的方式调用模板方法parallel_for

1
2
3
4
5
6
#include "tbb/tbb.h"

void ParallelApplyFoo(float a[], size_t n)
{
parallel_for(blocked_range<size_t>(0, n), ApplyFoo(a));
}

这里构造的blocked_range代表了从 0 到 n -1 的整个迭代区域。parallel_for会将此区域为每个处理器分出子区域。构造函数的一般形式是blocked_range<T>(begin, end, grainsize)。 T 指定了值的类型。 参数 begin 和 end 规定半开放区间[begin,end)作为该迭代区域的STL样式。参数 grainsize 后面会提到。例子使用默认的 grainsize值(1),因为默认情况下, parallel_for的启发式算法能在默认粒度下很好的工作。

采用lambda表达式,上面的例子可以写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "tbb/tbb.h"
using namespace tbb;

#pragma warning( disable: 588)

void ParallelApplyFoo(float *a, size_t n)
{
parallel_for(blocked_range<size_t>(0, n),
[=](const blocked_range<size_t>& r)
{
for (size_t i = r.begin(); i != r.end(); ++i)
Foo(a[i]);
});
}

为了更紧凑,对于在一个整形的连续区域执行并行循环,TBB有对应形式的parallel_for。表达式parallel_for(first,last,step,f)就像for(auto i = first; i< last; i+= step) f(i),只是在资源许可的情况下,每个f(i)可以并行求值。参数 step 是可选的。前面的例子可以重写为如下紧凑形式:

1
2
3
4
5
6
7
8
9
10
11
12
#include "tbb/tbb.h"
using namespace tbb;

#pragma warning(disable: 588)

void ParallelApplyFoo(float a[], size_t n)
{
parallel_for(size_t(0), n, [=](size_t i)
{
Foo(a[i]);
});
}

紧凑形式只能支持整形的线性迭代空间。自动分块特性将在下面介绍。

自动分块

并行循环的构造导致它调度工作的每个分块额外的开销。从2.2 版本开始,Intel TBB 视负载平衡所需自动选择分块尺寸。TBB采用的启发式算法会限制开销,同时为负载均衡提供足够的可选项。

注意:典型地,一个至少需要100万个时钟周期的循环才能使用parallel_for来提高性能。例如,在一个2GHz的处理器上需要500微秒的循环是可以从parallel_for 受益的。

对于大部分应用,推荐使用默认的自动分块。然而,伴随大多数启发式算法,总有一些更精确地控制块的尺寸会产生更好性能的情况。下一节会解释。

控制分块

分块是通过分区(partitioner)和粒度(grainsize)控制的。为了分块时获得最大的控制权,两者都需要指定。

  • 指定simple_partitioner()作为parallel_for的第三个参数。关闭自动分块。

指定构造区间时的粒度。这里讨论的构造形式为:blocked_range<T>(begin,end,grainsize)grainsize的默认值为1,它是每个块的循环迭代的单位。如果块太小,间接的开销可能更甚于有用的工作。

上节的例子修改为使用显式的粒度 G :

1
2
3
4
5
6
7
#include "tbb/tbb.h" 

void ParallelApplyFoo( float a[], size_t n )
{
parallel_for(blocked_range<size_t>(0,n,G), ApplyFoo(a),
simple_partitioner());
}

粒度为并行设置了最低门槛。例子中的parallel_for在块上(大小不见得一样)调用ApplyFoo::operator()。让块尺寸作为在块上迭代的数量。使用simple_partitioner确保[G/2] <= chunksize <= G

使用auto_partitioneraffinity_partitioner时,可以仅为区间(range)指定粒度,这是一种中等级别的控制。auto_partitioner是默认的分区器。两个分区器都实现了“自动分块”一节中描述的自动粒度启发式算法。affinity_partitioner实现了额外的窍门(在下面的“带宽与缓存亲缘性”一节中解释)。虽然这些分区器可能导致超出 G 迭代数量的块,但不会产生少于 [G/2] 迭代的块。分区器在启发式算法失败时会产生浪费性的小块,虽然偶然,但显式指定区间粒度会很有用。

带宽与缓存(cache)亲缘性

对于足够简单的函数 Foo, 编写成并行循环的例子也许不能展现出良好的加速效果。原因可能是处理器与内存间的系统带宽不足。这种情况下,你可能要重新考虑算法以便更好地利用缓存(cache)。为更好地利用缓存进行重构通常会使程序(无论并行还是串行)受益。

某些情况下的重构的一种替代方案是affinity_partitioner。他不仅自动选择粒度,而且优化缓存的亲缘性。使用它在下列情况下会显著地改进性能:

  • 每次数据问题时,计算只有少量操作
  • 被循环访问的数据适合留在缓存中
  • 循环,或者类似的循环,在同样的数据上重复执行
  • 可用硬件线程的数量多于两个。如果只有两个线程可用,intel TBB 的默认调度会提供良好的缓存亲缘性。

下面的代码展示了如何使用affinity_partitioner

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "tbb/tbb.h"

void ParallelApplyFoo(float a[], size_t n)
{
static affinity_partitioner ap;
parallel_for(blocked_range<size_t>(0, n), ApplyFoo(a), ap);
}

void TimeStepFoo(float a[], size_t n, int steps)
{
for (int t = 0; t < steps; ++t)
ParallelApplyFoo(a, n);
}

在这个示例中,affinity_partitioner的对象ap存在于循环迭代中。它记着循环的迭代从哪里执行,这样每个迭代都能被以前执行它的线程处理。示例中将affinity_partitioner的对象示例声明为局部静态变量来得到ap正确的生存周期。另一种方法是将它定义在TimeStepFoo函数中循环体的外面, 传递给parallel_for的调用链。

分区器总结

并行循环模板parallel_for以及parallel_reduce接受一个可选的partitioner 参数,通过它指定执行循环的策略。下表总结了三种分区器,以及当与blocked_range联合使用时的效果。

  • simple_partitioner:以粒度为单位选择块大小
  • auto_partitioner:自动选择块大小
  • affinity_partitioner:自动选择块大小以及缓存亲缘性

auto_partitioner在不指定分区器的情况下使用。一般来说, 应该使用auto_partitioner或者affinity_partitioner,因为他们基于有效的执行资源来制定块的数量。然而,在下述情况下,simple_partitioner是可用的:

  • operator()的子区域(subrange)不能超出某个限度。 这可能是有利的。例如,如果你的operator()需要一个跟区域大小成正比的临时数组。子区域的大小限定了,你就可以为这个数组使用一个自动变量而不是使用动态内存分配。
  • 大尺度的子区域不能有效使用缓存。例如,假定一个子区域的处理流程需要重复清理同一块内存区域。保持子区域在某个限度下可以使重复引用的内存区域适合放入缓存。
  • 你想调整为某个特定的机器。

parallel_reduce

循环可以做减量,像这样:

1
2
3
4
5
6
7
float SerialSumFoo(float a[], size_t n)
{
float sum = 0;
for (size_t i = 0; i != n; ++i)
sum += Foo(a[i]);
return sum;
}

如果迭代是独立的,你可以使用模板类parallel_reduce来并行化这个循环:

1
2
3
4
5
6
float ParallelSumFoo( const float a[], size_t n )
{
SumFoo sf(a);
parallel_reduce( blocked_range<size_t>(0,n), sf );
return sf.my_sum;
}

SumFoo指定了降低的细节,诸如怎么累加子总和并将它们合并。下面是SumFoo的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SumFoo 
{
float* my_a;
public:
float my_sum;
void operator()( const blocked_range<size_t>& r )
{
float *a = my_a;
float sum = my_sum;
size_t end = r.end();
for( size_t i=r.begin(); i!=end; ++i )
sum += Foo(a[i]);
my_sum = sum;
}

SumFoo( SumFoo& x, split ) : my_a(x.my_a), my_sum(0) {}

void join( const SumFoo& y ) {my_sum+=y.my_sum;}

SumFoo(float a[] ) :
my_a(a), my_sum(0)
{}
};

注意与parallel_for章节中提到的ApplyFoo类的区别。第一,operator()不是const。这是因为它必须更新SumFoo::my_sum。第二,SumFoo提供分割构造函数以及一个join方法以使parallel_reduce工作。分割构造函数需要两个参数,其一,一个指向原始对象的引用,其二,一个类型为split(TBB库中定义) 的哑元参数。这个哑元参数将分割构造函数与拷贝构造函数区分开。

提示:实例中,operator()的定义为访问标量值在循环内部使用局部临时变量(a, sum, end)。这种技术通过明白告诉编译器这些值可以放在缓存中而不是内存中来提高性能。如果这些值过大不适合放进寄存器,或者以一种编译器不能追踪的方式获取地址,这项技术就没用了。在一个典型的优化编译器中,为只写变量(如例子中的 sum )使用局部临时变量应该足够了。因为随后编译器就能推断这个循环不会写任何其他的位置,并将其他的读取提升到循环外。

当任务调度器确定工作者线程有效时,parallel_reduce调用分割构造函数为工作者创建子任务。当子任务完工后,parallel_reduce使用join方法 来累加子任务的结果。

如果没有工作者线程可用,迭代的第二半约减操作时就使用第一半使用过的同一个实体对象。它开始的地方,就是第一半结束的地方。

小心: 因为分割/合并在没有有效工作者时不能派上用场, parallel_reduce 没有必要做递归分割。

小心:因为同一个实体(body)可能被用来累加多个子区域, operator() 不能丢弃早先的累加值就至关重要了。下面的代码展示了一种错误定义SumFoo::operator()的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SumFoo
{
....
public:
float my_sum;
void operator()( const blocked_range<size_t>& r )
{
...
float sum = 0; // WRONG – should be "sum = my_sum".
...
for( ... )
sum += Foo(a[i]);
my_sum = sum;
}
...
};

由于错误的函数实现,operator()只是返回了应用parallel_reduce后最后一个子区域而不是所有子区域的值。parallel_reduce的分区器与粒度的规则跟parallel_for是一样的。

parallel_reduce归纳了所有相关操作。通常,分割构造函数会做两件事:

  • 拷贝必要的只读信息来运行循环体
  • 初始化约减操作标识元素的变量
  • join 方法做相应的合并操作。你可以在同一时间做多个约减操作:可以使用单个parallel_reduce 同时搜集最大、最小

注意:约减(reduction)操作可以是不可交换的。例子中浮点数加法如果替换成了字符串连接,同样可行。

高级示例

一个高级点的联合操作的例子是找到最小Foo(i)的索引。串行版本是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
long SerialMinIndexFoo( const float a[], size_t n ) 
{
float value_of_min = FLT_MAX; // FLT_MAX from <climits>
long index_of_min = -1;
for( size_t i=0; i<n; ++i )
{
float value = Foo(a[i]);
if( value<value_of_min )
{
value_of_min = value;
index_of_min = i;
}
}
return index_of_min;
}

循环的工作方式就是保持最终找到的最小值以及这个值的索引。这是循环迭代间携带的唯一信息。为了将此循环转换成parallel_reduce, 函数对象(operator())必须保持追踪这个携带信息,并知道如何在这些迭代跨越多个线程时合并这个信息。同样,函数对象必须记录一个指向 a 的指针来提供上下文。

下面的代码展示了完整的函数对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MinIndexFoo
{
const float *const my_a;
public:
float value_of_min;
long index_of_min;
void operator ()(const blocked_range<size_t>& r)
{
const float *a = my_a;
for (size_t i = r.begin(); i != r.end(); ++i)
{
float value = Foo(a[i]);
if (value < value_of_min)
{
value_of_min = value;
index_of_min = i;
}
}
}

MinIndexFoo(MinIndexFoo& x, split) :
my_a(x.my_a),
value_of_min(FLT_MAX), // FLT_MAX from <climits>
index_of_min(-1)
{ }

void join(const SumFoo& y)
{
if (y.value_of_min < value_of_min)
{
value_of_min = y.value_of_min;
index_of_min = y.index_of_min;
}
}

MinIndexFoo(const float a[]) :
my_a(a),
value_of_min(FLT_MAX), // FLT_MAX from <climits>
index_of_min(-1),
{ }
};

现在可以使用parallel_reduce来重写SerialMinIndex了:

1
2
3
4
5
6
long ParallelMinIndexFoo(float a[], size_t n)
{
MinIndexFoo mif(a);
parallel_reduce(blocked_range<size_t>(0, n), mif);
return mif.index_of_min;
}

截至目前,所有的示例都使用blocked_range<T>类来指定区域。这个类可以在很多情况下使用,但并非适用所有的情况。你可以使用Intel Threading Building Blocks 定义自己的迭代空间对象。这个对象必需提供两个方法以及一个“分割构造函数”指定将其自身分割为子空间的方式。如果这个类叫R, 方法以及构造函数会是下面这样:

1
2
3
4
5
6
7
8
9
10
class R 
{
// True if range is empty
bool empty() const;
// True if range can be split into non-empty subranges
bool is_divisible() const;
// Split r into subranges r and *this
R( R& r, split );
...
};

如果区域为空,empty()返回 true. 如果区域可被分割为两个非空子区域,而且这个分割带来的好处多于带来的损耗,is_divisible 就返回 true. 分割构造函数有两个参数:

  • 第一个类型为 R
  • 第二个类型为 tbb::split
  • 第二个参数没用;它只是为了将这个构造函数与普通的拷贝构造函数区分开。分割构造函数会试图将 r 大约分成两个等分, 将 r 更新为第一个等分,将构造出来的对象作为第二个等分。这两个等分都应该是非空的。并行算法模板在只有 r.is_divisible 为 true 的情况下才在 r 调用分割构造函数。

迭代空间不用必须是线性的。tbb/blocked_range2d.h 就是个二维区域的示例。它的分割构造函数试图沿着最长的坐标轴分割此区域。当与parallel_for 一起使用时,它以使循环陷入“递归阻塞”的方式来改进缓存使用。这种漂亮的缓存行为意味着在 blocked_ranged2d 上使用 parallel_for 能让循环比对应的串行版本运行的更快,即使是在单个的处理器上。

互斥

互斥控制某块代码能同时被多少线程执行。在Intel Threading Building Blocks(intelTBB)中,互斥通过互斥体(mutexes)和锁(locks)来实现。互斥体是一种对象,在此对象上,一个线程可以获得一把锁。在同一时间,只有一个线程能持有某个互斥体的锁,其他线程必须等待时机。

最简单的互斥体是spin_mutex。试图在spin_mutex上获得锁的线程要保持繁忙等待,直到成功。spin_mutex适合一个锁只被持有数个指令时常的情况。例如,下面的代码使用一个互斥体FreeListMutex来保护一个共享变量FreeList。它负责审查在同一时间只有一个线程访问FreeList。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Node* FreeList;
typedef spin_mutex FreeListMutexType;
FreeListMutexType FreeListMutex;
Node* AllocateNode()
{
Node* n;
{
FreeListMutexType::scoped_lock lock(FreeListMutex);
n = FreeList;
if (n)
FreeList = n->next;
}
if (!n)
n = new Node();
return n;
}
void FreeNode(Node* n)
{
FreeListMutexType::scoped_lock lock(FreeListMutex);
n->next = FreeList;
FreeList = n;
}

scoped_lock的构造子(构造函数)会一直等待,直到FreeListMutex上没有别的锁。析构子(析构函数)释放获得的锁。AllocateNode中的大括弧也许看起来不太常见。它们的作用是使锁的生命周期尽可能的短,这样其他的正在等待的线程就能尽可能快地得到机会。

注意:确保命名锁对象,否则它会被过快的销毁。例如,如果例子中的scoped_lock对象以如下方式创建

1
FreeListMutexType::scoped_lock (FreeListMutex);

这样scoped_lock会在执行到分号处时销毁,即在FreeList被访问前释放锁。

编写AllocatedNode的另一种可选方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Node* AllocateNode()
{
Node* n;
FreeListMutexType::scoped_lock lock;
lock.acquire(FreeListMutex);
n = FreeList;
if (n)
FreeList = n->next;
lock.release();
if (!n)
n = new Node();
return n;
}

acquire方法在得到锁前会一直等待;release方法释放该锁。

推荐的做法是尽可能得加上大括弧,以使得那些代码被锁保护对于维护者来说更为清晰。

如果你很熟悉锁的C接口,也许会疑惑为什么在互斥体对象自身上没有获取、释放方法。原因是C接口不是异常安全的,因为如果被保护的区域抛出一个异常,控制流就会略过释放操作。借助面向对象接口,析构scoped_lock对象会致使锁的释放,无论是正常退出保护区域,还是因为异常。即使对于我们使用acquire、release方法实现的AllocateNode的版本也是这样的——显式释放让锁得以早点释放,而后,析构函数判断锁已经被释放,就不去操作锁了。

Intel TBB中所有的互斥体都有类似的接口,不但能让他们易于学习,还能适用于泛型编程。例如,所有的互斥体都嵌套一个scoped_lock类型,对于给定类型M,对应的锁类型是M::scoped_lock

推荐为互斥体类型使用typedef,如同前面的例子所示。以这种方式,你可以稍后改变锁的类型而不用编辑其余的代码。在这些例子中,可以使用typedef queuing_mutex FreeListMutexType来代替typedef spin_mutex FreeListMutexType(及使用queuing_mutex代替spin_mutex),代码仍然正确。

互斥体要素

互斥体的行家总结了互斥体的各种特性。知道这些是有帮助的,因为它们影响通用性、性能的权衡。选择正确会有助于性能提升。互斥体能以下面的要素描述:

  • 可伸缩性 一些互斥体被称为可伸缩的。在严格意义上,这不是一个准确的名字,因为互斥体限制在某个时间某个线程的执行。一个可伸缩的互斥体是不会比这个做的更差。如果等待线程消耗了大量的处理器循环和内存带宽,减少了线程做实际工作的速度,此时互斥体会比串行执行更糟糕。在轻微竞争的情况下,可伸缩互斥体通常要比非可伸缩互斥体要慢,此时非可伸缩互斥体要优于前者。如果有疑惑,就使用可伸缩互斥体。
  • 公平 互斥体可以是公平或者非公平的。公平的互斥体按照线程到达的顺序使其通过,防止饿死线程。每个线程依序进行。然而,非公平互斥体会更快,它们允许正在运行的线程先通过,而不是下一个也许因为某个中断正在睡眠的在线(in line)线程。
  • 递归 互斥体可以是递归的,也可以是非递归的。可递归互斥体允许线程在持有此互斥体锁的情况下再次获得锁。这在一些递归算法中很有用,但也增加了锁实现的开销。
  • 放弃或者阻塞 这是影响性能的实现细节。在长等待时,Intel TBB的互斥体要么放弃(yields)要么阻塞(blocks)。这里的放弃(yields)的意思是,重复轮询看能否有进展,如果不能,就暂时放弃处理器的使用权。阻塞意味着直到互斥体完成处理才释放处理器。如果等待短暂,就使用放弃互斥体;如果等待时间往往比较长,就使用阻塞互斥体。(在windows系统中,yield通过SwitchToThread()实现,其他系统中通过sched_yield()实现)

下面是互斥体的行为总结:

  • spin_mutex 非可伸缩,非公平,非递归,在用户空间自旋(光吃不干)。看起来它似乎在所有场景里都是最坏的,例外就是,在轻微竞争的情况下,它非常快。如果你设计程序时,竞争行为在很多spin_mutex对象间传播,那还是使用别的种类的互斥体为好。如果互斥体是重度竞争的,你的算法无论如何都不会是可伸缩的。此种情况下,重新设计算法比寻找更有效的锁合适。
  • queuing_mutex 可伸缩,公平,非递归,在用户控件自旋。当可伸缩与公平很重要时使用。
  • spin_rw_mutexqueuing_rw_mutex 与spin_mutex、queuing_mutex类似,但是增加了读取锁支持。
  • mutexrecursive_mutex 这两个互斥体是对系统原生互斥的包装。在windows系统中,是在CRITICAL_SECTION(关键代码段)上封装的。在Linux以及Mac OS 操作系统中,通过pthread的互斥体实现。封装的好处是加入了异常安全接口,并相比Intel TBB的其他互斥体提供了接口的一致性,这样当出于性能方面考虑时能方便地将其替换为别的互斥体。
  • null_mutexnull_rw_mutex 这两个互斥体什么都不做。它们可被用作模版参数。例如,假定你要定义一个容器模板并且知道它的一些实例会被多个线程共享,需要内部锁定,但是其余的会被某个线程私有,不需要锁定。你可以定义一个将互斥体类型作为参数的模板。在需要锁定时,这个参数可以是真实互斥体类型中的一种,在不需要锁定时,将null_mutex作为参数传入。

读写锁

互斥在当多个线程写操作某个共享变量时是必要的。但允许多个读操作者进入保护区域就没什么大不了了。互斥体的读写变种,在类名称中以_rw_标记,通过区分读取锁与写入锁,允许多个读操作者。一个给定的互斥体,可以有多个读取锁。

scoped_lock的构造函数通过一个额外的布尔型参数来区分读取锁请求与写入锁请求。如果这个参数为false,表示请求读取锁。true表示请求写入锁。默认值为true,这样,当省略此参数时,spin_rw_mutex或者queuing_rw_mutex的行为就跟没有_rw_的版本一样。

升级/降级

通过方法upgrade_to_writer可以将一个读取锁升级为写入锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::vector<string> MyVector;
typedef spin_rw_mutex MyVectorMutexType;
MyVectorMutexType MyVectorMutex;
void AddKeyIfMissing(const string& key)
{
// Obtain a reader lock on MyVectorMutex
MyVectorMutexType::scoped_lock
lock(MyVectorMutex,/*is_writer=*/false);
size_t n = MyVector.size();
for (size_t i = 0; i<n; ++i)
if (MyVector[i] == key) return;
if (!MyVectorMutex.upgrade_to_writer())
// Check if key was added while lock was temporarily released
for (int i = n; i<MyVector.size(); ++i)
if (MyVector[i] == key) return;
vector.push_back(key);
}

注意,vector在某些时候必须重新搜索。这是因为upgrade_to_writer在它升级前可能不得不临时释放锁。否则,接下来可能会发生死锁(下面会讲到)。upgrade_to_writer方法返回值为bool类型,在没有释放锁就成功升级的情况下会返回true,如果锁被临时释放了,返回false。因此,如果upgrade_to_writer返回了false,代码必须重新运行查找操作确保“key”没有被其他的线程插入。例子假定“keys”总被追加到vector的末端,而且这些键值不会被移除。由于这些假定,它不用重新搜索整个vector,而仅搜索那些最初搜索过的之外的元素。需要记住的关键点是,如果upgrade_to_writer返回了false,任何假定持有读取锁的假定都可能无效,必须重新检查。

于此相应,有个相对的方法downgrade_to_reader,但是在实际应用中,基本找不到使用它的理由。

锁异常

锁会导致性能与正确性问题。对于使用锁的新手,有些问题要避免:

  • 死锁:当多个线程企图获得多个锁,而且它们会相互持有对方需要的锁时,死锁就会发生。更为准确地定义,当发生以下情况时死锁会发生:
  • 存在线程回路:每个线程至少持有互斥体上的一个锁,而且在等待回路中下一个线程已经持有锁的互斥体
  • 任何线程都不愿意放弃它的锁:避免需要同一时间持有两把锁的情况。将大块的程序拆分为小块,每块都可以在持有一把锁的情况下完工。
  • 总是以同样的顺序取锁。例如,如果你有“外部容器”与“内部容器”互斥体,需要从中获取锁,你可以总是先从“外部密室”获取。另外一个例子是在锁具有命名的情况下“以字母顺序获取锁”。或者,如果锁没有命名,就以互斥体的数字地址作为顺序获取锁。

锁护送

另外一个与锁相关的常见问题是锁护送。当操作系统打断一个持有锁的线程时,这种情况就会发生。所有其他的需要这把锁的线程都必须等待被中断的线程恢复并释放锁。公平互斥体会导致更糟糕的状况,因为,如果一个正在等待的线程被中断,所有它后面的线程都必须等待它恢复(就不单是需要它持有锁的那些线程的问题了)。

要最小化这种情况发生,应该尽量缩短持有锁的时间。在请求锁之前,进行任何可被预先计算的工作。

要避免这种情况,尽可能使用原子操作代替锁。

任务调度

Intel Threading Building Blocks (Intel® TBB)是基于任务(task)驱动的。一般来说,只有在TBB提供的算法模板中找不到合适的模板时,才考虑使用任务调度器自行实现。任务(task)是一个逻辑概念,操作系统并没有提供对应的实现。你可以把它当作线程池的进化。实现时,一个thread可对应多个task。在非阻塞编程时,相对于线程(thread),基于任务的编程有很多优点,比如:

  • task的启动、停止通常比thread更快
  • task更能匹配有效资源(因为有TBB的任务调度器)
  • task在编程时使程序员更能专注业务实现而不是底层细节
  • task实现了负载均衡

但是,要记住,task的应用场景是并行,而不是并发(不要企图把TBB用于Socket之类的并发敲打)。如果一个task被阻塞,其对应的thread也将被阻塞,这样,运行于thread之上的所有task都将被阻塞。

任务对象的生成

task的定义在task.h中,派生类必须要实现纯虚函数execute

1
2
//! Should be overridden by derived classes.
virtual task* execute() = 0;

task对象不能直接new,而是要使用TBB中重载的new操作符:

1
2
3
4
5
inline void *operator new( size_t bytes, const tbb::internal::allocate_root_proxy& ) 
inline void *operator new( size_t bytes, const tbb::internal::allocate_root_with_context_proxy& p )
inline void *operator new( size_t bytes, const tbb::internal::allocate_continuation_proxy& p )
inline void *operator new( size_t bytes, const tbb::internal::allocate_child_proxy& p )
inline void *operator new( size_t bytes, const tbb::internal::allocate_additional_child_of_proxy& p )

下面是TBB Tutorial中的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <tbb/task.h>
#include <tbb/tick_count.h>
#include <cstdio>

using tbb::task;

long SerialFib(long n)
{
if (n < 2)
return n;
else
return SerialFib(n - 1) + SerialFib(n - 2);
}

class FibTask : public task
{
public:
const long n;
long* const sum;
FibTask(long n_, long* sum_) :
n(n_), sum(sum_)
{
}
task* execute()
{
if (n < 10)
{
*sum = SerialFib(n);
}
else
{
long x, y;
FibTask& a = *new(allocate_child()) FibTask(n - 1, &x);
FibTask& b = *new(allocate_child()) FibTask(n - 2, &y);
// ref_count的值为2+1(a+b+后面函数sapwn_and_wait_for_all产生的等待任务)
set_ref_count(3);
spawn(b);
spawn_and_wait_for_all(a);
*sum = x + y;
}
return NULL;
}
};

long ParallelFib(long n)
{
long sum;
FibTask& a = *new(task::allocate_root()) FibTask(n, &sum);
task::spawn_root_and_wait(a);
return sum;
}

int main(int argc, char** argv)
{
using namespace tbb;
tick_count start = tick_count::now();
ParallelFib(10);
tick_count end = tick_count::now();
printf("tick count = %f\n", (end - start).seconds());

return 0;
}

任务的调度

调度器持有一个定向图表,每个节点对应一个任务对象。每个task指向它的继任者(successor),也就是指向等待它完成的任务(可以为空)。successor可以通过task::parent()得到。每个任务对象都包含一个引用计数,用来统计将此任务作为继任者的任务数量”。下图是斐波那契计算的任务图形快照:

任务A、B、C都产生了子任务并等待其完成。它们的引用计数为子任务的数目+1.

任务D正在运行,但是没有产生子任务,所以不需要设置引用计数

任务E、F、G都没有开始执行(spawned,当时没有excuting)

调度器运行任务的方式倾向于最小化内存需求以及跨线程通讯。但也需要在两种执行方式(深度优先、广度优先)间达到平衡。假定树是固定的,深度优先就是最佳的顺序执行方式:

  • 趁热打铁 最深层次的通常是最新创建的任务,因此在缓存(cache)中处于活跃状态。如果他们能完成,紧接着他们的任务就会被执行(比如D执行完后执行C),虽然不如第一个任务在缓存中的状态活跃,但相比创建事件更久的任务,它是最有效的。
  • 最小化空间占用 执行最浅节点的任务会将树按照广度优先展开。这将同时创建指数级数量的节点。于此相比,深度优先只创建同等数量的节点,而且同一时间存在一个线性数量,因为它将其他准备好的任务压入堆栈。

虽然广度优先有着严重的内存占用问题,但在如果你拥有无数个物理线程,它能最大并行化。一般来说物理线程都是有限的,所以广度优先执行的数量让有效的处理器保持繁忙就够了。调度器实现了广度优先、深度优先的混合执行模式。每个线程都有自己的就绪任务队列。当一个线程产出一个任务时,就将此任务推入队列的底部。

线程的队列

线程执行任务的时候,按照以下规则从任务队列取得任务:

  • 规则1:获取上一个task的execute方法返回的task,如果为空继续获取
  • 规则2:从自身的队列底部弹出一个task,如果队列为空,继续下一条判断
  • 规则3:随机选择一个任务队列,从其顶部“偷”一个task。如果选择的队列为空,继续遍历其余的队列,直到成功

规则2的效果就是执行本线程最近产出的任务,属于深度优先执行任务。规则3会从别的线程任务队列中选择最先产出的任务,发生广度优先任务执行,将潜在的并行变为实际的并行执行。作为任务演进图的一部分,获取任务是自动的。任务入队可以是显式的,也可以是隐式的。一个线程总是把任务加入自己队列的底部(不会加入另外线程的队列)。只有偷窃器才能把一个线程产出的任务传送到另外一个线程。在以下条件下,一个线程会将一个任务压入它的队列:

  • 任务被此线程显式产出,比如方法spawn
  • 一个任务被方法task::recycle_to_reexecute标记为再执行
  • 一个线程执行完最后的前任任务,并且此后隐式地将任务的引用计数减少到0。如果这种情况发生,线程隐式的将后续任务推入他的队列底部。如果一个任务有外部引用,执行完它所有的孩子任务并不会导致它的引用计数为0

总体来说,任务调度的基本策略是“广度优先窃取,深度优先运行”。广度优先窃取准则会使线程保持繁忙,提升并行效率。深度优先运行准则会使每个线程在有足够工作需要做时,保持高效操作。

有用的任务技术

递归链式反应

如果任务图为树形结构,调度器能工作的最好。因为此时“广度优先窃取、深度优先执行”策略非常适合。而且,树形结构的任务图也能很快地为很多任务创建出来。比如,一个主控任务需要创建N个孩子,如果直接创建,需要O(N)个步骤。但使用树形结构叉分建立,只需要O(lg(N))个步骤。

一般情况下,问题都不是明显的树形结构,但可以轻松将他们映射到树。比如,parallel_for工作在迭代空间(比如,一个整数队列)。模板函数parallel_for使用定义将一个迭代空间递归映射到一个二叉树。

持续传递

spawn_and_wait_for_all方法使正在执行的父任务等待所有的子任务完成,但是会稍微影响一些性能。当一个线程调用这个函数时,它会保持繁忙直到所有的孩子任务完成。有些时候,父任务准备就绪,可以继续执行,但却不能马上开始,因为它的线程还在执行其他任务中的一个任务。解决方案是父任务不再等待它的孩子,而是产出子任务后返回。子任务不是被作为父任务的孩子被分配,而是作为父任务的持续任务(continuation task)。这样,空闲的线程在它的子任务完成后就能偷窃并运行持续任务。上述FibTask的“持续传递”变体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct FibContinuation : public task
{
long* const sum;
long x, y;
FibContinuation(long* sum_) : sum(sum_) {}
task* execute()
{
*sum = x + y;
return NULL;
}
};
struct FibTask : public task
{
const long n;
long* const sum;
FibTask(long n_, long* sum_) :
n(n_), sum(sum_)
{
}
task* execute()
{
if (n<10)
{
*sum = SerialFib(n);
return NULL;
}
else
{
FibContinuation& c =
*new(allocate_continuation()) FibContinuation(sum);
FibTask& a = *new(c.allocate_child()) FibTask(n - 2, &c.x);
FibTask& b = *new(c.allocate_child()) FibTask(n - 1, &c.y);
// 这里的引用计数是2,而不是2+1.
c.set_ref_count(2);
spawn(b);
spawn(a);
return NULL;
}
}
};

两个版本的以下不同点需要了解:

  • 最大的区别是,在execute方法中,原来版本的x、y都是局部变量。在持续传递版本,它们就不能是局部变量了,因为父任务在子任务完成之前就返回了。作为替代方案,他们都是持续任务FibContinuation的字段。
  • 改为使用allocate_continuation分配持续的任务。它与allocate_child类似,只是它的继任者(successor)是c而不是this,并且设置this的继任者为NULL,下面的图示了这种转换:

这种转换的一个属性就是它不改变继任者的引用计数,这样就避免了涉入引用计数逻辑。

引用计数被设置为2,子任务的数量。在初始版本,它被设置为3,因为spawn_and_wait_for_all需要增加计数。而且,代码设置持续任务(FibContinuation)而不是父任务的引用计数,因为是持续任务对象在等待子任务。

指针sum通过FibContinuation的构造函数传递给持续任务对象,因为现在是FibContinuation把计算结果保存到*sum。子任务仍然使用allocate_child分配,但是都作为c,而不是父节点的孩子。这样,当两个子任务完成后,就是c而不是this作为继任者被产出。如果你凑巧使用this.allocate_child(),父任务就会在两个子任务完成后再次运行。

如果大家还记得初始版本中的ParallelFib是怎么编写的,就也许会担心持续传递风格会打破这段代码,因为现在根FibTask在子任务完工之前完成,并且实现代码使用spawn_root_and_wait来等待根FibTask。这算不上问题,因为spawn_root_and_wait被设计的能与持续传递风格很好的工作。调用spawn_root_and_wait(x)并不真的等待x结束。实际上,它构造了X的一个亚元(dummy)继任者,并且等待继任者的引用计数被消减。因为allocate_continuation将此亚元继任者传递给持续任务,亚元继任者的引用计数会在持续任务完成后才递减。

调度旁路

调度旁路(scheduler bypass)是一种优化手段,此时你直接指定下一个要运行的任务。持续传递风格经常会为调度旁路开启机会。例如,在持续传递例子的最后,方法execute()产出任务“a”后返回。这会导致正在执行的线程做以下事情:

  1. 将任务“a”入栈线程的任务队列
  2. 从方法execute()返回
  3. 将任务“a”出栈,如果它被别的线程“偷窃”

步骤1、3都是不必要的队列操作,更坏的是,允许“偷窃”会损害局部性而没有显著增加并行。方法execute()能通过返回一个指向“a”的指针而不是产出它来避免这些问题。由线程执行任务的规则1可知,“a”变为此线程的下一个要执行的任务。而且,这种方法保证执行任务“a”的是此线程,而不是另外的线程。

下面的示例显示了前一节的例子中必须要做的变更:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct FibTask : public task
{
...
task* execute()
{
if (n<CutOff)
{
*sum = SerialFib(n);
return NULL;
}
else
{
FibContinuation& c =
*new(allocate_continuation()) FibContinuation(sum);
FibTask& a = *new(c.allocate_child()) FibTask(n - 2, &c.x);
FibTask& b = *new(c.allocate_child()) FibTask(n - 1, &c.y);
// Set ref_count to "two children".
c.set_ref_count(2);
spawn(b);
spawn(a);
//return NULL;
return &a;
}
}
};

任务再生

不但可以绕过调度器,也可以绕过任务分配与再分配。这在递归任务执行调度旁路时,会有相应的更高几率发生。考虑前面的例子。当它创建了一个持续任务“c”,会执行下面的步骤:

  1. 创建子任务“a”
  2. 创建并产出子任务“b”
  3. execute()方法返回指向任务“a”的指针
  4. 销毁父任务

如果把“a”当作父任务,就可以避免上述的步骤1、4. 在很多场景中,步骤1需要从父任务中拷贝状态。将“a”当作父任务会消除拷贝开销。下面的例子显示了使用任务再生改造调度旁路的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct FibTask : public task
{
/*const*/ long n;
long* /*const*/ sum;
...
task* execute()
{
if (n<10)
{
*sum = SerialFib(n);
return NULL;
}
else
{
FibContinuation& c =
*new(allocate_continuation()) FibContinuation(sum);
FibTask& a = *new(c.allocate_child()) FibTask(n - 2, &c.x);
FibTask& b = *new(c.allocate_child()) FibTask(n - 1, &c.y);
recycle_as_child_of(c);
n -= 2;
sum = &c.x;
// Set ref_count to "two children".
c.set_ref_count(2);
spawn(b);
//return &a;
return this;
}
}
};

execute()方法现在返回this,而不是”a” 任务。调用recycle_as_child_of(c)有几种作用:

  • 标记this在execute()返回后不能自动销毁
  • 设置this的继任者为c

为了防止引用计数问题,recycle_as_child_of有个前置条件,那就是this的继任者必须为空。这是在allocate_continuation发生后的情况。下图显示了allocate_continuation、recycle_as_child_of如何转换任务图:

使用任务再生时,确保原始任务的字段在任务开始运行后不能处于被使用状态。例子使用调度旁路技术来确保这点。可以在产出时,当它的字段没有被使用时再产出再生任务。这个限制甚至适用于任何const字段,因为产出(spawning)后,任务可能在父任务没有任何动作的情况下运行并销毁。

一个类似的方法,task::recycle_as_continuation(),将一个任务作为一个持续任务而不是孩子任务。

总结

由于任务调度的复杂性,官方并不鼓励直接使用调度器,采用parallel_forparallel_reduce等模板是个好主意。以下细节需要谨记:

  • 使用new(allocation_method)T来分配一个taskallocation_methodtask类的一种分配方法)。不要创建局部或者文件作用域的task实例
  • 除非使用allocate_additional_child_of,否则在运行任何任务前,它的兄弟任务都必须分配完毕。
  • 采用持续传递、绕过调度器,以及任务再生等技术榨取最大性能
  • 如果一个任务完成了,并且没有被标记为再执行,就会自动销毁。同样,它的继任者的引用计数会减少,如果到了0,继任者会被自动产出

内存分配

Intel Threading Building Blocks(Intel TBB)提供了两种与STL模板类(std::allocator)类似的内存分配器模板。这两类模板(scalable_allocator<T>cache_aligned_allocator<T>)解决并行编程中的如下关键问题:

  • 可伸缩性 当在线程中使用原本为串行编程而设计的内存分配器因单个同一时间只允许一个线程分配的共享池而竞争的时候,可伸缩性的问题就会凸显。使用内存分配模板scalable_allocator来避免此类可伸缩性瓶颈。这个模板可以提升急速分配、释放内存程序的性能。
  • 伪共享 当两个线程访问同一缓存行的不同字节时,伪共享的问题就会出现。这是因为,缓存行(cache line)是不同处理器缓存间交换信息的单位。如果一个处理器修改了一个缓存行而另外一个处理器读(或者写)同一个缓存行,那么它必须从一个处理器移动到另外一个处理器,即使两个处理处理的是这行内的不同字节。因为缓存行的移动会耗费数百个时钟周期,伪共享会损害性能。

使用cache_aligned_allocator<T>类在某个缓存行分配。两个使用cache_aligned_allocator分配的对象能被确保不会使用伪共享。如果一个对象使用cache_aligned_allocator<T>分配,而另外一个对象使用了不同的方式,就没有了这种保证。cache_aligned_allocator<T>的接口类似std::allocator,所以你可以将它作为allocator参数传递给STL的模板类。

下面的代码展示了如何声明一个使用cache_aligned_allocator作为分配器的STL vector:

1
std::vector<int,cache_aligned_allocator<int> >;

cache_aligned_allocator<T>的设计功能的实现伴随着空间开销,因为它必须至少分配一条缓存行占用的内存,即使是对很小的对象。所以,如果伪共享不成问题,就别使用cache_aligned_allocator<T>。可伸缩内存分配器包含了Intel的PSL CTG团队开发的McRT技术。

动态库的选择

scalable_allocator<T>模板需要Intel TBB 可伸缩内存分配器库。它并不需要Intel TBB的常规库,并且能与Intel TBB独立开来使用。如果没有指定可伸缩分配器库,模板tbb_allocator<T>cache_aligned_allocator<T>就会使用mallocfree等标准库提供的内存分配函数。因此,甚至可以在忽略可伸缩内存分配器库的应用中使用这些模板。Intel Threading Building Blocks的其余部分,有没有Intel TBB可伸缩内存分配器库都可以使用。

自动替换malloc等C/C++动态内存分配函数

在windows、Linux操作系统中,可以自动使用Intel TBB中相应的可伸缩实现替换所有标准动态内存分配函数调用(比如:malloc)。在一些场合,可以提升性能。

Linux C/C++动态内存借口替换

替换通过代理库(release:libtbbmalloc_proxy.so.x、debug:libtbbmalloc_proxy_debug.so.x)提供。替换行为可以通过运行时加载代理库(通过LD_PRELOAD)或者链接(linking)代理库实现。代理库实现了以下动态内存函数:

  • C library:malloc,calloc,realloc,free
  • 标准POSIX函数:posix_memalign
  • 废弃的函数:valloc,memalign,pvalloc,mallopt
  • 全局C++操作符:new、delete

动态加载时,要保证代理库以及相应的可伸缩内存分配器库可被访问。要做到这点,可通过在LD_LIBRARY_PATH中包含或者将其加入到/etc/ld.so.conf

下面是一个如何设置LD_PRELOAD以及链接程序使用替换的例子。

1
2
3
4
# Set LD_PRELOAD so that loader loads release version of proxy 
LD_PRELOAD=libtbbmalloc_proxy.so.2
# Link with release version of proxy and scalable allocator
g++ foo.o bar.o -ltbbmalloc_proxy -ltbbmalloc -o a.out

使用Debug版本的库:

1
2
3
4
# Set LD_PRELOAD so that loader loads debug version of proxy 
LD_PRELOAD=libtbbmalloc_proxy_debug.so.2
# Link with debug version of proxy and scalable allocator
g++ foo.o bar.o -ltbbmalloc_proxy_debug -ltbbmalloc_debug -o a.out

windows下C++动态内存接口替换

替换通过代理库(release:tbbmalloc_proxy.dll,debug:tbbmalloc_debug_proxy.dll)提供。能以下面的任一种方式实现:

  • 包含头文件 #include “tbb/tbbmalloc_proxy.h”
  • 设置链接参数
    • 对于32位代码:tbbmalloc_proxy.lib /INCLUDE:”___TBB_malloc_proxy” (三个下划线)
    • 对于64位代码:tbbmalloc_proxy.lib /INCLUDE:”__TBB_malloc_proxy” (两个下划线)

代理库实现了下面的动态内存函数:

  • 标准C运行时动态内存函数:malloc,calloc,realloc,free
  • 全局C++操作符:new,delete
  • Microsoft C运行时库函数:_msize

同样要保证代理库、可伸缩内存分配库在程序启动时能被加载,例如,可将其路径包含在%PATH%环境变量中。

原子操作

概述

可以使用原子操作来避免使用互斥。当一个线程执行原子操作,在其他线程眼里,这个操作是瞬时完成的。原子操作的优点是,相比较锁操作是快速的,而且不用为死锁、锁护送等问题而烦恼。缺点是,它们只有有限的一组操作,常常无法和成为有效的复杂操作。尽管如此,也不应该放弃使用原子操作替换互斥的机会。aotmic<T>类以C++风格实现了原子操作。

原子操作的一个典型应用是线程安全的引用计数。设x是类型为 int 的引用计数,当它变为0时程序需要做一些操作。在单线程代码中,你可以使用 int 来定义 x,然后--x;if ( x==0 ) action()。但在多线程环境中,这种方法可能会失效,因为两个线程可能以下表的方式交替操作(其中的t(x)代表机器的寄存器)。

下表列出了原子操作模板的5种基本操作:

  • = x:读取 x 的值
  • x =:给 x 赋值,并返回它
  • x.fetch_and_store(y):执行x=y,并返回x的旧值
  • x.fetch_and_add(y):执行x+=y,并返回x的旧值
  • x.compare_and_swap(y,z):如果x==z,执行 x=y . 返回x的旧值

因为这些操作都是自动的,它们可被在安全应用而不用互斥体。考虑下面的例子:

1
2
3
4
5
atomic<unsigned> counter;
unsigned GetUniqueInteger()
{
return counter.fetch_and_add(1);
}

例程 GetUniqueInteger 每被调用一次就返回一个不同的整形,直到计数器又从头计数。无论多少个线程同时执行这段代码,都不会出例外。

compare_and_swap 是很多非阻塞算法的基本操作。互斥体的一个问题是,如果持有某个锁的线程挂起了,其他所有线程在它恢复之前都会被阻塞。非阻塞算法用原子操作代替锁来避免这个问题。他们(非阻塞算法)通常很复杂,而且需要复杂的分析去验证。然而,下面的习惯很直观,值得知晓。它以一种基于 globalx 旧值的方式更新 globalx 。

1
2
3
4
5
6
7
8
9
10
11
12
13
atomic<int> globalx;
int UpdateX()
{ // Update x and return old value of x.
do
{
// Read globalX
oldx = globalx;
// Compute new value
newx = ...expression involving oldx....
// Store new value if another thread has not changed globalX.
} while (globalx.compare_and_swap(newx, oldx) != oldx);
return oldx;
}

比较差的情况下,一些线程迭代循环直到没有其他的线程干预。一般来说,如果更新只需要少数指令,这种方法要快于相应的互斥体解决方案。

注意:如果下述序列不利于你的意图,那么上述的更新方法就不可取:

  • 一个线程从 globalx 中读取值 A
  • 其他的线程将 globalx 从 A 修改为 B ,再到 A
  • 步骤1 的线程执行 compare_and_swap, 读取 A ,但没有检测到期间变化到 B

这个问题被称为 ABA 问题。为链表数据结构设计设计非阻塞算法时,它常常成为问题。

atomic没有构造函数

atomic<T>模板类特意没有声明构造函数,因为诸如上述的 GetUniqueInteger 之类的例子一般要求在所有的文件作用域构造函数被调用前就可以工作。如果该模板类声明了构造函数,在它被引用后,也许要初始化一个文件作用域的实例。在下述上下文中,任何没有生命构造函数的 C++类的原子类型atomic<T>的对象 X 被自动初始化为 0 :

  • X 被声明为文件作用域变量,或者类的静态数据成员
  • X 是类的成员,并且显式地出现在该类的构造函数的初始化列表中

下面的代码是对这些问题的解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
atomic<int> x; // 由于处于文件作用域,初始化为0 
class Foo
{
atomic<int> y;
atomic<int> notzeroed;
static atomic<int> z;
public:
Foo() :
y() // y 初始化为0.
{
// notzeroed has unspecified value here.
}
};
atomic<int> Foo::z; // 静态成员,初始化为0

异常与终止

Intel TBB支持异常与终止(cancellation),当算法中的代码抛出异常时,会按依次发生:

  • 捕获异常。算法内进一步的异常被忽略。
  • 算法终止。挂起的迭代操作不被执行。如果内部存在嵌套的Intel TBB并行,那么它的取消与否取决于特定实现(下面会提到)
  • 算法的所有部分都停止后,会在调用算法的线程(thread)上抛出异常。

步骤3中抛出的异常可能是初始的异常,也可能仅仅是captured_exception类型的摘要。后者常发生在当前的系统中,因为在线程间传递异常需要支持C++的std::exception_ptr机制。随着编译器在支持此项特性上的进展,将来的Intel TBB版本可能抛出初始的异常。所以,确保你的代码可以捕获两种异常中的任意异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "tbb/tbb.h"
#include <vector>
#include <iostream>

using namespace tbb;
using namespace std;

vector<int> Data;

struct Update {
void operator ()(const blocked_range<int>& r) const
{
for (int i = r.begin(); i != r.end(); ++i) Data.at(i) += 1;
}
};

int main()
{
Data.resize(1000);
try
{
parallel_for(blocked_range<int>(0, 2000), Update());
}
catch (captured_exception& ex)
{
cout << "captured_exception: " << ex.what() << endl;
}
catch (out_of_range& ex)
{
cout << "out_of_range: " << ex.what() << endl;
}
return 0;
}

无异常终止

要取消某个算法而不抛出异常,使用表达式task::self().cancel_group_execution(). 其中的task::self()引用当前线程最靠内的Intel TBB任务。调用cancel_group_execution()取消它的task_group_context中的所以线程(下节会详细介绍)。如果的确导致了任务终止,此方法会返回 true ,如果task_group_context 已经被取消,就会返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "tbb/tbb.h"
#include <vector>
#include <iostream>

using namespace tbb;
using namespace std;

vector<int> Data;

struct Update {
void operator ()(const blocked_range<int>& r) const
{
for (int i = r.begin(); i != r.end(); ++i) if (i < Data.size())
{
++Data[i];
}
else
{
// Cancel related tasks.
if (task::self().cancel_group_execution())
cout << "Index " << i << " caused cancellation\n";
return;
}
}
};

int main()
{
Data.resize(1000);
parallel_for(blocked_range<int>(0, 2000), Update());
return 0;
}

第一章 结构化绑定

结构化绑定允许你使用对象的成员或者说元素来初始化多个变量。

举个例子,假如你定义了一个包含两个不同成员的结构:

1
2
3
4
5
6
struct MyStruct {
int i = 0;
std::string s;
};

MyStruct ms;

只需使用下面的声明,你就可以将这个结构体的成员直接绑定到新名字上
1
auto [u,v] = ms;

在这里,名字u和v就被称为结构化绑定(structured bindings)。在某种程度上,它们分解了对象并用来初始化自己(在有些地方它们也被称为分解声明(decompose declarations))。

结构化绑定对于那些返回结构体或者数组的函数来说尤其有用。举个例子,假设你有一个返回结构体的函数:

1
2
3
MyStruct getStruct() {
return MyStruct{42, "hello"};
}

你可以直接为函数返回的数据成员赋予两个局部名字:
1
auto[id,val] = getStruct(); // id and val name i and s of returned struct

在这里,id和val分别表示返回的数据成员i和s。它们的类型分别是int和std::string ,可以当新变量使用。
1
2
3
if (id > 30) {
std::cout << val;
}

使用结构化绑定的好处是可以直接通过名字访问值,并且由于名字可以传递语义信息,使得代码可读性也大大提高。

下面的示例展示了结构化绑定如何改善代码可读性。在没有结构化绑定的时候,要想迭代处理std::map<>的所有元素,需要这么写:

1
2
3
for (const auto& elem : mymap) {
std::cout << elem.first << ": " << elem.second << '\n';
}

代码中的elem是表示键和值的std::pair,它们在std::pair中分别用first和second表示,你可以使用这两个名字去访问键和值。使用结构化绑定后,代码可读性大大提高:
1
2
3
for (const auto& [key,val] : mymap) {
std::cout << key << ": " << val << '\n';
}

我们可以直接使用每个元素的键和值,key和value清晰的表示了它们的语义。

1.1 结构化绑定的细节

为了理解结构化绑定,了解其中设计的一个匿名变量是很重要的。结构化绑定引入的新名字都是指代的这个匿名变量的成员/元素的。

绑定到匿名变量

初始化代码的最精确的行为:

1
auto [u,v] = ms;

可以看成我们初始化一个匿名变量e,然后让结构化绑定u和v成为这个新对象的别名,类似下面:
1
2
3
auto e = ms;
aliasname u = e.i;
aliasname v = e.s;

注意u和v不是e.ie.s的引用。它们只是这两个成员的别名。因此,decltype(u)的类型与成员i的类型一致,decltype(v)的类型与成员s的类型一致。因为匿名变量e没有名字,所以我们不能直接访问这个已经初始化的变量。所以
1
std::cout << u << ' ' << v << ✬\n✬;

输出e.ie.s的值,它们是ms.ims.s的一份拷贝。

e和结构化绑定的存活时间一样长,当结构化绑定离开作用域时,e也会析构。

这样做的后果,除非使用引用,否则修改通过结构化绑定的值不会影响到初始化它的对象(反之亦然):

1
2
3
4
5
6
MyStruct ms{42,"hello"};
auto [u,v] = ms;
ms.i = 77;
std::cout << u; // prints 42
u = 99;
std::cout << ms.i; // prints 77

u和ms.i地址是不一样的。

当对返回值使用结构化绑定的时候,上面的规则一样成立。下面代码的初始化:

1
auto [u,v] = getStruct();

和我们使用getStruct()的返回值初始化匿名变量e,然后用u和v作为e的成员别名效果一样,类似下面:
1
2
3
auto e = getStruct();
aliasname u = e.i;
aliasname v = e.s;

换句话说,结构化绑定将绑定到一个新的对象,它由返回值初始化,而不是直接绑定到返回值本身。

对于匿名变量e,内存地址和对齐也是存在的,以至于如果成员有对齐,结构化绑定也会有对齐。比如:

1
2
auto [u,v] = ms;
assert(&((MyStruct*)&u)->s == &v); // OK

((MyStruct*)&u)会产生一个指向匿名变量的指针。

使用修饰符

我们在结构化绑定过程中使用一些修饰符,如const和引用。再次强调,这些修饰符修饰的是匿名变量e。虽说是对匿名变量使用修饰符,但是通常也可以看作对结构化绑定使用修饰符,尽管存在一些额例外。

下面的例子中,我们对结构化绑定使用const引用:

1
const auto& [u,v] = ms; // a reference, so that u/v refer to ms.i/ms.s

这里,匿名变量被声明为const引用,这意味着对ms使用const引用修饰,然后再将u和v作为i和s的别名。后续对ms成员的修改会直接影响到u和v:
1
2
ms.i = 77;      // affects the value of u
std::cout << u; // prints 77

如果使用非const引用,你甚至可以通过对结构化绑定的修改,影响到初始化它的对象:
1
2
3
4
5
6
MyStruct ms{42,"hello"};
auto& [u,v] = ms; // the initialized entity is a reference to ms
ms.i = 77; // affects the value of u
std::cout << u; // prints 77
u = 99; // modifies ms.i
std::cout << ms.i; // prints 99

如果初始化对象是临时变量,对它使用结构化绑定,此时临时值的生命周期会扩展:
1
2
3
4
MyStruct getStruct();
...
const auto& [a,b] = getStruct();
std::cout << "a: " << a << '\n'; // OK

修饰符并非修饰结构化绑定

如题,修饰符修饰的是匿名变量。它们没必要修饰结构化绑定。事实上:

1
const auto& [u,v] = ms;  // a reference, so that u/v refer to ms.i/ms.s

u和v都没有声明为引用。上面只是对匿名变量e的引用。u和v的类型需要ms的成员一致。根据我们最开始的定义可以知道,decltype(u)是int,decltype(v)std::string

当指定对齐宽度的时候也有一些不同。

1
alignas(16) auto [u,v] = ms;

在这里,我们将初始化后的匿名对象对齐而不是结构化绑定u和v。这意味着u作为第一个成员,被强制对齐到16位,而v不是。

同样的原因,尽管使用了auto,结构化绑定的类型也不会类型退化(术语退化(decay)描述的是当参数值传递的时候发生的类型转换,这意味着数组会转换为指针,最外面的修饰符如const和引用会被忽略)。例如,如果我们有一个包含多个原生数组的结构体:

1
2
3
4
struct S{
const char x[6];
const char y[3];
};

然后
1
2
S s1{};
auto [a, b] = s1; // a and b get the exact member types

a的类型仍然是const char[6]。原因仍然是修饰符并非修饰结构化绑定而是修饰初始化结构化绑定的对象。这一点和使用auto初始化新对象很不一样,它会发生类型退化:
1
auto a2 = a;    // a2 gets decayed type of a

移动语义

即将介绍到,结构化绑定也支持移动语义。在下面的声明中:

1
2
MyStruct ms = { 42, "Jim" };
auto&& [v,n] = std::move(ms); // entity is rvalue reference to ms

结构化绑定v和n指向匿名变量中的成员,该匿名变量是ms的右值引用。ms仍然持有它的值:
1
std::cout << "ms.s: " << ms.s << '\n'; // prints "Jim"

但是你可以移动赋值n,它与ms.s关联:
1
2
3
4
std::string s = std::move(n); // moves ms.s to s
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints unspecified value
std::cout << "s: " << s << '\n'; // prints "Jim"

通常,移动后的对象的状态是有效的,只是包含了未指定的值(unspecified value)。因此,输出它的值是没有问题的,但是不能断言输出的东西一定是什么。

这一点和直接移动ms的值给匿名变量稍有不同:

1
2
MyStruct ms = { 42, "Jim" };
auto [v,n] = std::move(ms); // new entity with moved-from values from ms

此时匿名对象是一个新对象,它用移动后的ms的值来初始化。所以ms失去了他们的值:
1
2
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints "Jim"

你仍然可以移动n并赋值,或者用它赋予一个新的值,但是不会影响ms.s
1
2
3
4
5
std::string s = std::move(n); // moves n to s
n = "Lara";
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints "Lara"
std::cout << "s: " << s << '\n'; // prints "Jim"

1.2 结构化绑定可以在哪使用

原则上,结构化绑定可以用于公有成员,原始C-style数组,以及“似若tuple”的对象:

  • 如果结构体或者类中,所有非静态数据成员都是public,那么你可以使用结构化绑定来绑定非静态数据成员
  • 对于原生数组,你可以使用结构化绑定来绑定每个元素
  • 对于任何类型,你都可以使用似若tuple的API来进行绑定。对于类型type,API可以粗糙的概括为下列内容:
    • std::tuple_size<type>::value返回元素数量
    • std::tupel_element<idx,type>::type返回第idx个元素的类型
    • 一个全局的或者成员函数get<idx>()返回第idx个元素的值

如果结构体或者累提供这些似若tuple的API,那么就可以使用它们。

任何情况下都要求元素或者数据成员的数量必须匹配结构化绑定的名字的个数。你不能跳过任何一个元素,也不能使用同一个名字两次。但是你可以看使用非常段的名字如”_”(很多程序员倾向于用下划线,但是也有些人讨厌它,不允许它出现在全局命名空间中),但是在一个作用域它也只能出现一次:

1
2
auto [_,val1] = getStruct(); // OK
auto [_,val2] = getStruct(); // ERROR: name _ already used

嵌套或者非平坦的对象分解是不支持的。(译注:指的是形如OCaml等语言的这种let a,(b,c) = (3,(4,2));;模式匹配能力)

接下来的章节讨论本节列表提到的各种情况。

1.2.1 结构体和类

到目前为止,已经演示了很多关于结构体和类的简单示例了。

如果类和结构体用到了继承,那么结构化绑定的使用就很受限了。所有非静态数据成员必须出现在同一个类。(换句话说,这些数据成员要么全是该类的,要么全是基类的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct B {
int a = 1;
int b = 2;
};

struct D1 : B {
};
auto [x, y] = D1{}; // OK

struct D2 : B {
int c = 3;
};

auto [i, j, k] = D2{}; // Compile-Time ERROR

1.2.1 原生数组

下面的代码使用有两个元素的C-style数组初始化x和y:

1
2
3
int arr[] = { 47, 11 };
auto [x, y] = arr; // x and y are ints initialized by elems of arr
auto [z] = arr; // ERROR: number of elements doesn’t fit

这种方式只能出现在数组长度已知的情况下。如果将数组作为参数传递,这样写就行不通,因为数组作为参数传递会发生类型退化,变成指针类型。

C++允许我们返回带长度的数组引用,如果有函数返回这种带长度的数组引用,那么也可以使用结构化绑定:

1
2
3
auto getArr() -> int(&)[2]; // getArr() returns reference to raw int array
...
auto [x, y] = getArr(); // x and y are ints initialized by elems of returned array

你也可以对std::array使用结构化绑定,但是这需要使用似若tuple的API,这也是下一节的内容。

1.2.3 std::paor,std::tuplestd::array

结构化绑定是可扩展的,你可以为任何类型添加结构化绑定机制。标准库为std::paor,std::tuplestd::array都添加了该机制。

std::array

举个例子,下面的getArray()将返回四个元素的std::array<>,并用它初始化i,j,k和l。

1
2
3
std::array<int,4> getArray();
...
auto [i,j,k,l] = getArray(); // i,j,k,l name the 4 elements of the copied return value

i,j,k和l分别绑定到getArray()返回的四个元素上。

写操作也是支持的,但这要求用来初始化结构化绑定的值不是一个临时的返回值:

1
2
3
4
std::array<int,4> stdarr { 1, 2, 3, 4 };
...
auto& [i,j,k,l] = stdarr;
i += 10; // modifies std::array[0]

std::tuple

下面的代码使用getTuple()返回有三个元素的std::tuple<>来初始化a,b和c:

1
2
3
std::tuple<char,float,std::string> getTuple();
...
auto [a,b,c] = getTuple(); // a,b,c have types and values of returned tuple

std::pair

另一个例子是处理关联型/无序型容器的insert()调用的返回值,使用结构化绑定使代码可读性更强,可以清晰的表达自己的意图,而不是依赖于std::tuple通用的first和second:

1
2
3
4
5
6
7
std::map<std::string, int> coll;
...
auto [pos,ok] = coll.insert({"new",42});
if (!ok) {
// if insert failed, handle error using iterator pos:
...
}

在C++17之前,必须使用下面的代码检查返回数据:
1
2
3
4
5
auto ret = coll.insert({"new",42});
if (!ret.second){
// if insert failed, handle error using iterator ret.first
...
}

注意,在这个例子中,C++17甚至还提供一种表达力更强的带初始化的if:

为pair和tuple的结构化绑定赋值

在声明了结构化绑定之后,通常你不能一次性修改全部结构化绑定,因为结构化绑定是一次性声明所有而不是一次性使用所有。然而,如果重新赋的值是std::pair<>或者std::tuple<>那么你可以使用std::tie()

也就是说,你可以写出下面的代码:

1
2
3
4
5
std::tuple<char,float,std::string> getTuple();
...
auto [a,b,c] = getTuple(); // a,b,c have types and values of returned tuple
...
std::tie(a,b,c) = getTuple(); // a,b,c get values of next returned tuple

这种方式在实现循环调用且每次循环赋予一对返回值的过程中尤其有用,比如下面子啊循环中使用searcher的代码:
1
2
3
4
5
6
std::boyer_moore_searcher bm{sub.begin(), sub.end()};
for (auto [beg, end] = bm(text.begin(), text.end());
beg != text.end();
std::tie(beg,end) = bm(end, text.end())) {
...
}

1.3 为结构化绑定提供似若tuple的API

前面提到过,只要你的类型实现了似若tuple的API,那么就可以针对该类型使用结构化绑定,就和标准库的std::pair<>,std::tuple<>std::array<>意义。

只读结构化绑定

下面的代码展示了如何为类型Customer添加结构化绑定功能,Customer的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// lang/customer1.hpp
#include <string>
#include <utility> // for std::move()
class Customer {
private:
std::string first;
std::string last;
long val;
public:
Customer (std::string f, std::string l, long v)
: first(std::move(f)), last(std::move(l)), val(v) {
}
std::string getFirst() const {
return first;
}
std::string getLast() const {
return last;
}
long getValue() const {
return val;
}
};

我们可以提供似若tuple的API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// lang/structbind1.hpp
#include "customer1.hpp" #include <utility> // for tuple-like API
// provide a tuple-like API for class Customer for structured bindings:
template<>
struct std::tuple_size<Customer> {
static constexpr int value = 3; // we have 3 attributes
};
template<>
struct std::tuple_element<2, Customer> {
using type = long; // last attribute is a long
};
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
using type = std::string; // the other attributes are strings
};
// define specific getters:
template<std::size_t> auto get(const Customer& c);
template<> auto get<0>(const Customer& c) { return c.getFirst(); }
template<> auto get<1>(const Customer& c) { return c.getLast(); }
template<> auto get<2>(const Customer& c) { return c.getValue(); }

代码Customer有三个成员,还有为三个成员准备的getter:

  • 表示first name的成员,std::string类型
  • 表示last nane的成员,std::string类型
  • 表示value的成员,long类型

获取Customer成员个数的函数是std::tuple_size的特化:

1
2
3
4
template<>
struct std::tuple_size<Customer> {
static constexpr int value = 3; // we have 3 attributes
};

获取成员类型的函数是std::tuple_element的特化:
1
2
3
4
5
6
7
8
template<>
struct std::tuple_element<2, Customer> {
using type = long; // last attribute is a long
};
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
using type = std::string; // the other attributes are strings
};

第三个成员类型是long,需要为它(index 2)编写全特化代码。其它成员是std::stinrg类型,部分特化(比全特化优先级低)即可。这里指定的类型与decltype产生的类型一致。

最终,我们在同一个命名空间为Customer类型定义相应的get<>()函数重载:

1
2
3
4
template<std::size_t> auto get(const Customer& c);
template<> auto get<0>(const Customer& c) { return c.getFirst(); }
template<> auto get<1>(const Customer& c) { return c.getLast(); }
template<> auto get<2>(const Customer& c) { return c.getValue(); }

在这里,我们声明了模板函数,然后为所有情况都写出来对应的全特化形式。

注意,模板函数的全特化必须与模板函数的签名一致(也包括一致的返回类型)。原因是我们只提供了特定的“实现”,而不是声明新的函数。下面的代码不能通过编译:

1
2
3
4
template<std::size_t> auto get(const Customer& c);
template<> std::string get<0>(const Customer& c) { return c.getFirst(); }
template<> std::string get<1>(const Customer& c) { return c.getLast(); }
template<> long get<2>(const Customer& c) { return c.getValue(); }

通过使用新的编译时if特性,我们可以所有特化形式的get<>()组合到一个函数里面:
1
2
3
4
5
6
7
8
9
10
11
12
template<std::size_t I> auto get(const Customer& c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.getFirst();
}
else if constexpr (I == 1) {
return c.getLast();
}
else { // I == 2
return c.getValue();
}
}

有了这些API,就能对Customer的对象使用结构化绑定了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
int main()
{
Customer c("Tim", "Starr", 42);
auto [f, l, v] = c;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
// modify structured bindings:
std::string s = std::move(f);
l = "Waters";
v += 10;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v <<'\n';
std::cout << "c: " << c.getFirst() << ' '
<< c.getLast() << ' ' << c.getValue() << '\n';
std::cout << "s: " << s << '\n';
}

和往常一样,结构化绑定f,l和v是新的匿名变量的成员的别名,新的匿名变量经由c初始化。初始化为每个成员调用相应的getter函数。因此,在初始化后,修改c不会影响到结构化绑定(反之亦然)。所以,程序的输出如下:
1
2
3
4
f/l/v: Tim Starr 42
f/l/v: Waters 52
c: Tim Starr 42
s: Tim

你也可以在迭代一个由Customer元素构成的vector的过程中使用结构化绑定:
1
2
3
4
5
std::vector<Customer> coll;
...
for (const auto& [first, last, val] : coll) {
std::cout << first << ' ' << last << ": " << val << '\n';
}

对结构化绑定使用decltype仍然回产出它的类型,而不是匿名变量的类型。这意味着decltype(first)const std::string

允许针对结构化绑定的写操作

似若tuple的API可以可以使用产生引用的函数。这使得我们可以允许针对结构化绑定的写操作发生。考虑下面的代码,它为Customer提供了读取和修改成员的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// lang/customer2.hpp
#include <string>
#include <utility> // for std::move()
class Customer {
private:
std::string first;
std::string last;
long val;
public:
Customer (std::string f, std::string l, long v)
: first(std::move(f)), last(std::move(l)), val(v) {
}
const std::string& firstname() const {
return first;
}
std::string& firstname() {
return first;
}
const std::string& lastname() const {
return last;
}
std::string& lastname() {
return last;
}
long value() const {
return val;
}
long& value() {
return val;
}
};

要支持读写操作,我们还得为常量引用和非常量引用准备getter重载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// lang/structbind2.hpp
#include "customer2.hpp"
#include < utility> // for tuple-like API
// provide a tuple-like API for class Customer for structured bindings:
template <> struct std::tuple_size<Customer> {
static constexpr int value = 3; // we have 3 attributes
};
template <> struct std::tuple_element<2, Customer> {
using type = long; // last attribute is a long
};
template <std::size_t Idx> struct std::tuple_element<Idx, Customer> {
using type = std::string; // the other attributes are strings
};
// define specific getters:
template <std::size_t I> decltype(auto) get(Customer &c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.firstname();
} else if constexpr (I == 1) {
return c.lastname();
} else { // I == 2
return c.value();
}
}
template <std::size_t I> decltype(auto) get(const Customer &c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.firstname();
} else if constexpr (I == 1) {
return c.lastname();
} else { // I == 2
return c.value();
}
}
template <std::size_t I> decltype(auto) get(Customer &&c) {
static_assert(I < 3);
if constexpr (I == 0) {
return std::move(c.firstname());
} else if constexpr (I == 1) {
return std::move(c.lastname());
} else { // I == 2
return c.value();
}
}

你应该写出这三个重载,来处理常量对象,非常量对象,以及可移动对象。为了返回引用,你应该使用decltype(auto)

还是之前那样,我们可以使用新的编译时if特性,来简化我们的实现,尤其是getter的返回类型不一样时,它更有用。没有编译时if特性,我们只能写出所有的全特化:

1
2
3
4
template<std::size_t> decltype(auto) get(Customer& c);
template<> decltype(auto) get<0>(Customer& c) { return c.firstname(); }
template<> decltype(auto) get<1>(Customer& c) { return c.lastname(); }
template<> decltype(auto) get<2>(Customer& c) { return c.value(); }

模板函数声明的签名必须与全特化的一致(包括返回类型)。下面的代码不能编译:
1
2
3
4
template<std::size_t> decltype(auto) get(Customer& c);
template<> std::string& get<0>(Customer& c) { return c.firstname(); }
template<> std::string& get<1>(Customer& c) { return c.lastname(); }
template<> long& get<2>(Customer& c) { return c.value(); }

做完这些后,你就能使用结构化绑定读取或者修改Customer的成员了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "structbind2.hpp" 
#include <iostream>
int main() {
Customer c("Tim", "Starr", 42);
auto [f, l, v] = c;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
// modify structured bindings via references:
auto &&[f2, l2, v2] = c;
std::string s = std::move(f2);
f2 = "Ringo";
v2 += 10;
std::cout << "f2/l2/v2: " << f2 << ' ' << l2 << ' ' << v2 << '\n';
std::cout << "c: " << c.firstname() << ' ' << c.lastname() << ✬ ✬ << c.value() << '\n';
std::cout << "s: " << s << '\n';
}

它会输出:
1
2
3
4
f/l/v: Tim Starr 42
f2/l2/v2: Ringo Starr 52
c: Ringo Starr 52
s: Tim

1.4 后记

结构化绑定最初由Herb Sutter,Bjarne Stroustrup和Gabriel Dos Reis在https://wg21.link/p0144r0中提出,当时使用花括号而不是方括号。最后这个特性的公认措辞是由Jens Maurer在https://wg21.link/p0217r3中给出。

第二章 带初始化的if和switch

现在ifswitch控制结构允许我们在普通的条件语句或者选择语句之外再指定一个初始化语句。

比如,你可以这样写:

1
2
3
if (status s = check(); s != status::success) {
return s;
}

其中初始化语句是:
1
status s = check();

它初始化s,然后用if判断s是否是有效状态。

2.1 带初始化的if

任何在if语句内初始化的值的生命周期都持续到then代码块或者else代码块(如果有的话)的最后。比如:

1
2
3
4
5
6
7
8
9
if (std::ofstream strm = getLogStrm(); coll.empty()) {
strm << "<no data>\n";
}
else {
for (const auto& elem : coll) {
strm << elem << '\n';
}
}
// strm no longer declared

strm的析构函数回在then代码块或者else代码块的最后调用。

另一个例子是执行一些依赖某些条件的任务的时候使用锁:

1
2
3
if (std::lock_guard<std::mutex> lg{collMutex}; !coll.empty()) {
std::cout << coll.front() << '\n';
}

因为有类模板参数推导,也可以这样写:
1
2
3
if (std::lock_guard lg{collMutex}; !coll.empty()) {
std::cout << coll.front() << '\n';
}

任何情况下,上面的代码都等价于:
1
2
3
4
5
6
{
std::lock_guard<std::mutex> lg{collMutex};
if (!coll.empty()) {
std::cout << coll.front() << '\n';
}
}

区别在于lg是在if语句的作用域中定义的,因此与条件在相同的作用域(声明性区域)中,就像for循环中初始化的情况一样。

任何被初始化的对象都必须有一个名字。否则,初始化语句会长久一个立即销毁大的临时值。举个例子,初始化一个没有名字的lock guard,其后的条件检查不是在加锁环境下进行的:

1
2
3
4
if (std::lock_guard<std::mutex>{collMutex}; // run-time ERROR:
!coll.empty()) { // - no longer locked
std::cout << coll.front() << '\n'; // - no longer locked
}

一般来说,一个_作为名字也是可以的(一些程序员喜欢它,另一些讨厌它因为它污染全局命名空间):
1
2
3
4
if (std::lock_guard<std::mutex> _{collMutex}; // OK, but...
!coll.empty()) {
std::cout << coll.front() << '\n';
}

接下来是第三个例子,考虑一段代码,插入新元素到map或者unordered map。你可以检查操作是否成功,就像下面一样:
1
2
3
4
5
6
7
std::map<std::string, int> coll;
...
if (auto [pos, ok] = coll.insert({"new", 42}); !ok) {
// if insert failed, handle error using iterator pos:
const auto &[key, val] = *pos;
std::cout << "already there: " << key << '\n';
}

这段代码还是用了结构化绑定,给返回值和元素插入的位置pos分别赋予了名字,而不是first和second。在C++17前,上面相应的检查必须像下面一样规范:
1
2
3
4
5
6
auto ret = coll.insert({"new", 42});
if (!ret.second) {
// if insert failed, handle error using iterator ret.first
const auto &elem = *(ret.first);
std::cout << "already there: " << elem.first << '\n';
}

注意这种带if的初始化也能用于编译时if特性。

2.2 带初始化的switch

使用带初始化的switch语句允许我们在检查条件并决定控制流跳转到哪个case执行之前初始化一个对象。

比如,我们可以先初始化一个文件系统路径,再根据路径的类型选择对应的处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using namespace std::filesystem;
...
switch (path p(name); status(p).type()) {
case file_type::not_found:
std::cout << p << " not found\n";
break;
case file_type::directory:
std::cout << p << ":\n";
for (auto &e : std::filesystem::directory_iterator(p)) {
std::cout << "- " << e.path() << '\n';
}
break;
default:
std::cout << p << " exists\n";
break;
}

初始化的p能在整个switch语句中使用。

2.3 后记

带初始化的if和switch最初由Thomas Koppe在https://wg21.link/p0305r0中提出,当时只有带初始化的if没有带初始化的switch。最后这个特性的公认措辞是由Thomas Koppe在https://wg21.link/p0305r1中给出。

第三章 内联变量

C++的一个优点是它支持header-only(译注:即只有头文件)的库。然而,截止C++17,header-only的库也不能有全局变量或者对象出现。

C++17后,你可以在头文件中使用inline定义变量,如果这个变量被多个翻译单元(translation unit)使用,它们都会指向相同对象:

1
2
3
4
5
class MyClass {
static inline std::string name = ""; // OK since C++17
...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

3.1 内联变量的动机

C++不允许在class内部初始化非const静态成员:

1
2
3
4
class MyClass {
static std::string name = ""; // Compile-Time ERROR
...
};

在class外面定义这个变量定义这个变量,且变量定义是在头文件中,多个CPP文件包含它,仍然会引发错误:
1
2
3
4
5
class MyClass {
static std::string name; // OK
...
};
MyClass::name = ""; // Link ERROR if included by multiple CPP files

根据一处定义规则(one definition 入了,ODR),每个翻译单元只能定义变量最多一次。

即便有预处理保护(译注:也叫头文件保护,header guard)也没有用:

1
2
3
4
5
6
7
8
#ifndef MYHEADER_HPP
#define MYHEADER_HPP
class MyClass {
static std::string name; // OK
...
};
MyClass.name = ""; // Link ERROR if included by multiple CPP files
#endif

不是因为头文件可能被包含多次,问题是两个不同的CPP如果都包含这个头文件,那么MyClass.name可能定义两次。

同样的原因,如果你在头文件中定义一个变量,你会得到一个链接时错误:

1
2
3
4
class MyClass {
...
};
MyClass myGlobalObject; // Link ERROR if included by multiple CPP files

临时解决方案

这里有一些临时的应对措施:

  • 你可以在class/struct内初始化一个static const整型数据成员:
    1
    2
    3
    4
    class MyClass {
    static const bool trace = false;
    ...
    };
  • 你可以定义一个返回局部static对象的内联函数:
    1
    2
    3
    4
    inline std::string getName() {
    static std::string name = "initial value";
    return name;
    }
  • 你可以定义一个static成员函数返回它的值:
    1
    2
    3
    4
    std::string getMyGlobalObject() {
    static std::string myGlobalObject = "initial value";
    return myGlobalObject;
    }
  • 你可以使用变量模板(C++14及以后):
    1
    2
    template<typename T = std::string>
    T myGlobalObject = "initial value";
  • 你可以继承一个包含static成员的类模板:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<typename Dummy>
    class MyClassStatics
    {
    static std::string name;
    };
    template<typename Dummy>
    std::string MyClassStatics<Dummy>::name = "initial value";
    class MyClass : public MyClassStatics<void> {
    ...
    };
    但是这些方法都有不小的负载,可读性也比较差,想要使用全局变量也比较困难。除此之外,全局变量的初始化可能会推迟到它第一次使用的时候,这使得应用程序不能在启动的时候把对象初始化好。(比如用一个对象监控进程)。

3.2 使用内联变量

现在,有了inline,你可以在头文件中定义一个全局可用的变量,它可以被多个CPP文件包含:

1
2
3
4
5
class MyClass {
static inline std::string name = ""; // OK since C++17
...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

初始化发生在第一个包含该头文件的翻译单元。

形式化来说,在变量前使用inline和将函数声明为inline有相同的语义:

  • 如果每个定义都是相同的,那么它可以在多个翻译单元定义
  • 它必须在使用它的每个翻译单元中定义

两者都是通过包含来自同一头文件的定义来实现的。最终程序的行为就像是只有一个变量。

你甚至可以在头文件中定义原子类型的变量:

1
inline std::atomic<bool> ready{false};

注意,对于std::atomic,通常在定义它的时候你还得初始化它。

这意味着,你仍然必须保证在你初始化它之前类型是完全的(complete)。比如,如果一个struct或者class有一个static成员,类型是自身,那么该成员只能在该类型被声明后才能使用。

1
2
3
4
5
6
7
8
9
struct MyType {
int value;
MyType(int i) : value{i} {
}
// one static object to hold the maximum value of this type:
static MyType max; // can only be declared here
...
};
inline MyType MyType::max{0};

参见另一个使用内联变量的例子,它会使用头文件跟踪所有new调用

3.3 constexpr隐式包含inline

对于static数据成员,constexpr现在隐式包含inline的语义,所以下面的声明在C++17后会定义static数据成员n:

1
2
3
4
struct D {
static constexpr int n = 5; // C++11/C++14: declaration
// since C++17: definition
};

换句话说,它与下面的代码一样:
1
2
3
struct D {
inline static constexpr int n = 5;
};

在C++17之前,有时候你也可以只声明不定义。考虑下面的声明:
1
2
3
struct D {
static constexpr int n = 5;
};

如果不需要D::n的定义,这就足够了,例如,D::n只通过值传递的话:
1
std::cout << D::n; // OK (ostream::operator<<(int) gets D::n by value)

如果D::n是传引用到非内联函数,并且/或者函数调用没有优化,那么就是无效的。比如:
1
2
int inc(const int& i);
std::cout << inc(D::n); // usually an ERROR

这段代码违背了一处定义规则(ODR)。当使用带优化的编译器构建时,它可能正常工作,或者抛出链接时错误指出缺少定义。当使用不带优化的编译器时,几乎可以确定这段代码会由于缺少D::n的定义而拒绝编译:

因此,在C++17前,你不得不在相同的翻译单元定义D::n

1
2
constexpr int D::n; // C++11/C++14: definition
// since C++17: redundant declaration (deprecated)

当使用C++17构建,在class中的声明本身就是一个定义,所以这段代码就算没有前面的定义也是有效的。前面的定义也是可以的,但是已经废弃。

3.4 内联变量和thread_local

使用thread_local你可以让每个线程拥有一个内联变量:

1
2
3
4
5
6
struct ThreadData {
inline static thread_local std::string name; // unique name per thread
...
};

inline thread_local std::vector<std::string> cache; // one cache per thread

为了演示一个完整的例子,考虑下面的头文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// lang/inlinethreadlocal.hpp
#include <string>
#include <iostream>

struct MyData {
inline static std::string gName = "global"; // unique in program
inline static thread_local std::string tName = "tls"; // unique per thread
std::string lName = "local"; // for each object
...
void print(const std::string& msg) const {
std::cout << msg << '\n';
std::cout << "- gName: " << gName << '\n';
std::cout << "- tName: " << tName << '\n';
std::cout << "- lName: " << lName << '\n'; }
};

inline thread_local MyData myThreadData; // one object per thread

你可以在有main()的翻译单元使用它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lang/inlinethreadlocal1.cpp
#include "inlinethreadlocal.hpp"
#include <thread>
void foo();

int main()
{
myThreadData.print("main() begin:");
myThreadData.gName = "thread1 name";
myThreadData.tName = "thread1 name";
myThreadData.lName = "thread1 name";
myThreadData.print("main() later:");
std::thread t(foo);
t.join();
myThreadData.print("main() end:");
}

你可以在另一个定义foo()的翻译单元使用头文件,其中foo()被不同的线程调用:
1
2
3
4
5
6
7
8
9
10
11
// lang/inlinethreadlocal2.cpp
#include "inlinethreadlocal.hpp"

void foo()
{
myThreadData.print("foo() begin:");
myThreadData.gName = "thread2 name";
myThreadData.tName = "thread2 name";
myThreadData.lName = "thread2 name";
myThreadData.print("foo() end:");
}

程序的输出如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main() begin:
- gName: global
- tName: tls
- lName: local
main() later:
- gName: thread1 name
- tName: thread1 name
- lName: thread1 name
foo() begin:
- gName: thread1 name
- tName: tls
- lName: local
foo() end:
- gName: thread2 name
- tName: thread2 name
- lName: thread2 name
main() end:
- gName: thread2 name
- tName: thread1 name
- lName: thread1 name

3.5 后记

David Krauss的文档https://wg21.link/n4147是内联变量产生的动机。内联变量最初由Hal Finkel和Richard Smith在https://wg21.link/n4424中提出。最后这个特性的公认措辞是由Hal Finkel和Richard Smith在 https://wg21.link/p0386r2中给出。

第四章 聚合扩展

C++中有一种初始化对象的方式叫做聚合初始化(aggregate initialization),它允许用花括号聚集多个值来初始化。

1
2
3
4
5
6
struct Data {
std::string name;
double value;
};

Data x{"test1", 6.778};

从C++17开始,聚合还支持带基类的数据结构,所以下面这种数据结构用列表初始化也是允许的:
1
2
3
4
struct MoreData : Data {
bool done;
};
MoreData y{{"test1", 6.778}, false};

正如你看到的,聚合初始化现在支持嵌套的花括号传给基类的成员来初始化。

对于带有成员的子对象的初始化,如果基类或子对象只有一个值,则可以跳过嵌套的大括号:

1
MoreData y{"test1", 6.778, false};

4.1 扩展聚合初始化的动机

如果没有这项特性的话,继承一个类之后就不能使用聚合初始化了,需要你为新类定义一个构造函数:

1
2
3
4
5
6
7
struct Cpp14Data : Data {
bool done;
Cpp14Data (const std::string& s, double d, bool b)
: Data{s,d}, done{b} {
}
};
Cpp14Data y{"test1", 6.778, false};

现在,有了这个特性我们可以自由的使用嵌套的花括号,如果只传递一个值还可以省略它:
1
2
MoreData x{{"test1", 6.778}, false}; // OK since C++17
MoreData y{"test1", 6.778, false}; // OK

注意,因为它现在是聚合体,其它初始化方式也是可以的:
1
2
MoreData u; // OOPS: value/done are uninitialized
MoreData z{}; // OK: value/done have values 0/false

如果这个看起来太危险了,你还是最好提供一个构造函数。

4.2 使用扩展的聚合初始化

关于这个特性的常见用法是列表初始化一个C风格的数据结构,该数据结构继承自一个类,然后添加了一些数据成员或者操作。比如:

1
2
3
4
5
6
7
8
9
10
11
12
struct Data {
const char* name;
double value;
};
struct PData : Data {
bool critical;
void print() const {
std::cout << ✬[✬ << name << ✬,✬ << value << "]\n"; }
};

PData y{{"test1", 6.778}, false};
y.print();

这里里面的花括号会传递给基类Data的数据成员。

你可以跳过一些初始值。这种情况下这些元素是零值初始化(zero initalized)(调用默认构造函数或者将基本数据类型初始化为0,false或者nullptr)。比如:

1
2
3
4
PData a{};          // zero-initialize all elements
PData b{{"msg"}}; // same as {{"msg",0.0},false}
PData c{{}, true}; // same as {{nullptr,0.0},true}
PData d; // values of fundamental types are unspecified

注意使用空的花括号和不使用花括号的区别。

  • a零值初始化所有成员,所以name被默认构造,double value被初始化为0.0,bool flag被初始化为false。
  • d只调用name的默认构造函数。所有其它的成员都没用被初始化,所以值是未指定的(unspecified)。

你也可以继承非聚合体来创建一个聚合体。比如:

1
2
3
4
5
6
7
8
9
10
struct MyString : std::string {
void print() const {
if (empty()) {
std::cout << "<undefined>\n"; }
else {
std::cout << c_str() << '\n'; } }
};

MyString x{{"hello"}};
MyString y{"world"};

甚至还可以继承多个非聚合体:
1
2
3
4
5
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};

然后使用下面的代码初始化它们:
1
2
3
4
5
D<float> s{{"hello"}, {4.5,6.7}, "world"};        // OK since C++17
D<float> t{"hello", {4.5, 6.7}, "world"}; // OK since C++17
std::cout << s.data; // outputs: ”world”
std::cout << static_cast<std::string>(s); // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s); // outputs: (4.5,6.7)

内部花括号的值(initializer_lists)会传递给基类,其传递顺序遵循基类声明的顺序。

这项新特性还有助于用很少的代码定义lambdas重载

4.3 聚合体定义

总结一下,C++17的聚合体(aggregate)定义如下:

  • 是个数组
  • 或者是个类类型(class,struct,union),其中
    • 没有用户声明的构造函数或者explicit构造函数
    • 没有使用using声明继承的构造函数
    • 没有private或者protected的非static数据成员
    • 没有virtual函数
    • 没有virtual,private或者protected基类

为了让聚合体可以使用,还要求聚合体没有private或者protected基类成员或者构造函数在初始化的时候使用。

C++17还引入了一种新的type trait即is_aggregate<>来检查一个类型是否是聚合体:

1
2
3
4
5
6
template<typename T>
struct D : std::string, std::complex<T> {
std::string data;
};
D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << std::is_aggregate<decltype(s)>::value; // outputs: 1 (true)

4.4 向后不兼容

注意,下面示例中的代码将不再能通过编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// lang/aggr14.cpp
struct Derived;
struct Base {
friend struct Derived;
private:
Base() {
}
};
struct Derived : Base {
};
int main()
{
Derived d1{}; // ERROR since C++17
Derived d2; // still OK (but might not initialize)
}

C++17之前,Derived不是一个聚合体,所以:
1
Derived d1{};

调用Derived隐式定义的默认构造函数,它默认调用基类Base的默认构造函数。虽然基类的默认构造函数是private,但是通过子类的默认构造函数调用它是有效的,因为子类被声明为一个friend类。

C++17开始,Derived是一个聚合体,没有隐式的默认构造函数。所以这个初始化被认为是聚合初始化,聚合初始化不允许调用基类的默认构造函数。不管基类是不是friend都不行。

4.5 后记

内联变量最初由Oleg Smolsky在https://wg21.link/n4404中提出。最后这个特性的公认措辞是由Oleg Smolsky在 https://wg21.link/p0017r1中给出。

新的type trait即std::is_aggregate<>最初作为美国国家机构对C++ 17标准化的评论而引入。(参见https://wg21.link/lwg2911

第五章 强制拷贝消除或者传递unmaterialized对象

本章的主题可以从两个角度来看:

  • C++17引入了新的规则,在确定条件下可以强制消除拷贝:以前临时对象传值或者返回临时对象期间发生的拷贝操作的消除是可选的,现在是强制的。
  • 因此,我们处理传递未具体化对象的值以进行初始化
    我将从技术上介绍这个特性,然后讨论具体化(materialization)的效果和相关术语。

5.1 临时量强制拷贝消除的动机

标准伊始,C++就明确允许一些拷贝操作可以被省略(消除),不调用拷贝构造函数会失去可能存在的副作用,从而可能影响程序的行为,即便这样也在所不惜。强制拷贝消除的场景之一是使用临时对象初始化新对象。这个情况经常发生,尤其是以值传递方式将临时对象传递给一个函数,或者函数返回临时对象。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass
{
...
};
void foo(MyClass param) { // param is initialized by passed argument
...
}
MyClass bar() {
return MyClass(); // returns temporary
}

int main()
{
foo(MyClass()); // pass temporary to initialize param
MyClass x = bar(); // use returned temporary to initialize x
foo(bar()); // use returned temporary to initialize param
}

但是,由于这些拷贝消除优化不是强制的,要拷贝的对象必须提供隐式或显式的拷贝或移动构造函数。也就是说,尽管拷贝/移动构造函数一般不会调用,但是也必须存在。如果没有定义拷贝/移动构造函数,那么代码不能通过编译。

因此,下面MyClass的定义的代码编译不了:

1
2
3
4
5
6
7
8
9
class MyClass
{
public:
...
// no copy/move constructor defined:
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) = delete;
...
};

这里没有拷贝构造函数就足够了,因为仅当没有用户声明的拷贝构造(或者拷贝赋值运算符)时移动构造函数才隐式可用。

C++17后,临时变量初始化新对象期间发生的拷贝是强制消除的。事实上,在后面我们会看到,我们简单的传值作为实参初始化或者返回一个值,该值会接下来用于具体化(materalize)一个新对象。

这意味着就算MyClass类完全没有表示启用拷贝操作,上面的例子也能通过编译。

然而,请注意其他可选的拷贝消除仍然是可选的,仍然要求一个可调用的拷贝或者移动构造函数,比如:

1
2
3
4
5
6
MyClass foo()
{
MyClass obj;
...
return obj; // still requires copy/move support
}

在这里,foo()里面的obj是一个带名字的变量(即左值(lvalue))。所以会发生命名的返回值优化(named return value optimization,NRVO),它要求类型支持拷贝或者移动操作。即便obj是一个参数也仍然如此:
1
2
3
4
5
MyClass bar(MyClass obj) // copy elision for passed temporaries
{
...
return obj; // still requires copy/move support
}

传递一个临时量(即纯右值(prvalue))到函数作为实参,不会发生拷贝/移动操作,但是返回这个参数仍然需要拷贝/移动操作,因为返回的对象有名字。

作为这一改变的部分,值范畴(value categories)修改和新增了很多术语。

5.2 临时量强制拷贝消除的好处

强制拷贝消除的一个好处是,很明显,如果拷贝操作开心较大时会得到更好的性能。虽然移动语言显著减少了拷贝开销,但是完全不执行拷贝能极大的提示性能。这可能会减少使用出参(译注:所谓出参即可out parameter,是指使用参数来传递返回信息,通常是一个指针或者引用)代替返回一个值(假设这个值是由返回语句创建的)的需求。

另一个好处是现在只要写一个工厂函数它总是能工作,因为现在的工厂函数可以返回对象,即便对象不允许拷贝/移动。比如,考虑下面的泛型工厂函数:

1
2
3
4
5
6
7
8
// lang/factory.hpp
#include <utility>
template <typename T, typename... Args>
T create(Args&&... args)
{
...
return T{std::forward<Args>(args)...};
}

这个函数现在甚至可以用于std::atomic<>这种类型,该类型既没有定义拷贝构造函数也没有定义移动构造函数:
1
2
3
4
5
6
7
8
9
10
// lang/factory.cpp
#include "factory.hpp"
#include <memory>
#include <atomic>

int main() {
int i = create<int>(42);
std::unique_ptr<int> up = create<std::unique_ptr<int>>(new int{42});
std::atomic<int> ai = create<std::atomic<int>>(42);
}

这个特性带来的另一个效果是,如果类有显式delete的移动构造函数,你现在可以返回临时值,然后用它初始化对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CopyOnly {
public:
CopyOnly() {
}
CopyOnly(int) {
}
CopyOnly(const CopyOnly&) = default;
CopyOnly(CopyOnly&&) = delete; // explicitly deleted
};

CopyOnly ret() {
return CopyOnly{}; // OK since C++17
}

CopyOnly x = 42; // OK since C++17

x的初始化代码在C++17之前是无效的,因为拷贝初始化需要将42转换为一个临时对象,然后临时对象原则上需要提供一个移动构造函数,尽管不会用到它。()

5.3 值范畴的解释

强制拷贝消除带来的额外工作是值范畴(value categories)的一些修改。

5.3.1 值范畴

在C++中的每个表达式都有一个值范畴。这个值范畴描述了表达式可以做什么。

值范畴的历史

从C语言历史的角度来看,在赋值语句中只有lvalue(左值)和rvalue(右值):

1
x = 42;

表达式x是lvalue,因为它可以出现在赋值语句的左边,表达式42是rvalue,因为它只能出现在赋值语句的右边。但是因为ANSI-C,事情变得更复杂一些,因为x如果声明为const int就不能在赋值语句的左边了,但是它仍然是个(不具可修改性的)lvalue。

C++11我们有了可移动的对象,这些对象在语义上是只能出现在赋值语句右边,但是可以被修改,因为赋值语句可以盗取它们的值。基于这个原因,新的值范畴xvalue被引入,并且之前的值范畴rvalue有了新名字即prvalue。

C++11的值范畴

C++11后,值范畴如图5.1描述的那样:我们的核心值范畴是lvalue,prvalue(pure rvalue,纯右值),xvalue(eXpiring value,将亡值)。组合得到的值范畴有:glvalue(generalized lvalue,泛化左值,是lvalue和xvalue的结合)以及rvalue(是xvalue和prvalue的结合)。

图5.1 C++11后的值范畴

lvalue的例子有:

  • 一个表达式只包含变量,函数或者成员的名字
  • 一个表达式是字符串字面值
  • 内置一元操作符*的结果(即对原生指针解引用)
  • 返回左值引用(type&)的函数的返回值

prvalue的例子有:

  • 除字符串字面值外的其他字面值(或者用户定义的字面值,其中与之关联的字面值操作符的返回类型标示值的范畴)
  • 内置一元操作符&的结果(即获取表达式地址)
  • 内置算术运算符的结果
  • 返回值的函数的返回值

xvalue的例子有:

  • 返回右值引用(type&&,尤其是返回std::move())的函数的返回值
  • 右值引用到对象类型的转换

大概来说:

  • 所有使用名字的表达式是lvalue
  • 所有字符串字面值表达式是lvalue
  • 所有其他字面值(4.2,true,nullptr)是prvalue
  • 所有临时变量(尤其是返回值的函数返回的对象)是prvalue
  • std::move()的结果是xvalue

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class X {
};

X v;
const X c;

void f(const X&); // accepts an expression of any value category
void f(X&&); // accepts prvalues and xvalues only, but is a better match

f(v); // passes a modifiable lvalue to the first f()
f(c); // passes a non-modifiable lvalue to the first f()
f(X()); // passes a prvalue to the second f()
f(std::move(v)); // passes an xvalue to the second f()

值得强调的是,严格来说,glvalue,prvalue和xvalue是针对表达式的, 不是针对值的(这意味着这些值用词不当)。举个例子,一个变量本身不是一个lvalue,只有一个变量放到表达式里才标示这个变量是lvalue:
1
2
int x = 3; // x here is a variable, not an lvalue
int y = x; // x here is an lvalue

第一个语句中3是prvalue,它用来初始化变量x(不是lvalue)。第二个语句中x是lvalue(对它求值会会发现它包含值3)。然后作为lvallue的x转换为prvalue,用来初始化变量y。

5.3.2 C++17的值范畴

C++17没有改变既有的值范畴,但是阐述了它们的语义(如图5.2所示)

图5.1 C++17后的值范畴

现在解释值范畴的主要方式是认为我们有两类表达式:

  • glvalue:对象/函数位置的表达式
  • prvalue:初始化表达式
    xvalue被认为是一个特殊的位置,表示有一个变量它的资源可以重用(通常因为它接近它的生命周期结尾)。

C++17引入了一个新术语,具体化(materialization),表示在某个时刻一个prvalue成为临时对象。因此,临时变量具体化转换(temporary materialization conversion)是指prvalue到xvalue的转换。

任何时刻,期待出现glvalue(lvalue或xvalue)的地方出现prvalue都是有效的,创建一个临时变量并通过prvalue初始化,然后prvallue被替换为xvalue。因此在上面的例子中,严格来说:

1
2
3
void f(const X& p); // accepts an expression of any value category,
// but expects a glvalue
f(X()); // passes a prvalue materialized as xvalue

因为例子中的f()有一个引用参数,它期待一个glvalue实参。然而,表达式X()是一个prvalue。临时具体化规则因此生效,表达式X()转换为一个xvalue并使用默认构造函数初始化临时变量。

注意具体化不意味着我们创建了一个新的/不同的对象。lvalue引用仍然绑定xvalue和prvalue,虽然后者总是转换到xvalue。

在这些改变后,拷贝消除意义非凡,因为prvalue不再要求可移动,我们只传递一个初始值,这个值迟早会具体化然后初始化一个对象。

5.4 未具体化返回值传递

未具体化返回值传递是指所有形式的返回临时对象(prvalue)的值:

  • 当返回一个不是字符串字面值的字面值:
    1
    2
    3
    int f1() {    // return int by value
    return 42;
    }
  • 当返回类型为临时变量的值或者使用auto:
    1
    2
    3
    4
    auto f2() {   // return deduced type by value
    ...
    return MyType{...};
    }
  • 当返回临时对象,并且类型用decltype(auto)推导:
    1
    2
    3
    4
    decltype(auto) f3() {   // return temporary from return statement by value
    ...
    return MyType{...};
    }
    记住如果用于初始化的表达式(这里是返回语句)会创建一个临时变量(prvalue),那么用decltype(auto)声明的类型是值。

上述所有形式我们都返回一个prvalue的值,我们不需要任何拷贝/移动的支持。

5.5 后记

强制拷贝消除最初由Richard Smith在https://wg21.link/p0135r0中提出。最后这个特性的公认措辞是由Richard Smith在https://wg21.link/p0135r1中给出。

第六章 Lambda扩展

C++11引入了lambda,C++14引入了泛型lambda,这是一个成功的故事。lambda允许我们将功能指定为参数,这让定制函数的行为变得更加容易。

C++ 17进一步改进,允许lambda用在更多的地方。

6.1 constexpr lambda

自C++17后,只要可能,lambda就隐式地用constexpr修饰。也就是说,任何lambda都可以用于编译时上下文,前提是它使用的特性对编译时上下文有效(例如,仅字符串字面值,无静态变量,无virutal变量,无try/catch,无new/delete)。

举个例子,你可以传一个值给lambda,然后用计算的结果作为编译时的std::array<>大小:

1
2
3
4
auto squared = [](auto val) { // implicitly constexpr since C++17
return val*val;
};
std::array<int,squared(5)> a; // OK since C++17 => std::array<int,25>

如果在不允许constexpr的上下文使用这个特性就不行,但是你仍然可以在运行时傻姑娘上下文使用lambda:
1
2
3
4
5
6
7
8
he lambda in run-time contexts:
auto squared2 = [](auto val) { // implicitly constexpr since C++17
static int calls = 0; // OK, but disables lambda for constexpr contexts
...
return val*val;
};
std::array<int,squared2(5)> a; // ERROR: static variable in compile-time context
std::cout << squared2(5) << '\n'; // OK

要知道是否一个lambda在一个编译时上下文有效,你可以将它声明为constexpr:
1
2
3
auto squared3 = [](auto val) constexpr {    // OK since C++17
return val*val;
};

还可以指定返回类型,语法如下:
1
2
3
auto squared3i = [](int val) constexpr -> int { // OK since C++17
return val*val;
};

constexpr对于函数的一般规则仍然有效:如果lambda在运行时上下文中使用,相应的功能在运行时执行。

然而,在不允许编译时上下文的地方使用constexpr lambda会得到一个编译时错误:

1
2
3
4
5
auto squared4 = [](auto val) constexpr {
static int calls=0; // ERROR: static variable in compile-time context
...
return val*val;
};

如果lambda式显式或隐式的constexpr,那么函数调用操作符也会是constexpr。换句话说,下面的定义:
1
2
3
auto squared = [](auto val) { // implicitly constexpr since C++17
return val*val;
};

会转换为闭包类型:
1
2
3
4
5
6
7
8
class CompilerSpecificName {
public:
...
template<typename T>
constexpr auto operator() (T val) const {
return val*val;
}
};

生成的闭包类型的函数调用操作符是自动附加constexpr的。在C++17中,如果lambda显式定义为constexpr或者隐式定义为constexpr(就像这个例子),那么生成的函数调用运算符也会是constexpr。

6.2 传递this的拷贝到lambda

当在成员函数中使用lambda时,你不能隐式的访问调用这个成员函数的对象的成员。也就是说,在lambda内部,如果不捕获this,那么你不能使用这个对象的成员:

1
2
3
4
5
6
7
8
9
10
11
class C {
private:
std::string name;
public:
...
void foo() {
auto l1 = [] { std::cout << name << '\n'; }; // ERROR
auto l2 = [] { std::cout << this->name << '\n'; }; // ERROR
...
}
};

C++11和C++14中可以传this引用或者传this值:
1
2
3
4
5
6
7
8
9
10
11
12
class C {
private:
std::string name;
public:
...
void foo() {
auto l1 = [this] { std::cout << name << '\n'; }; // OK
auto l2 = [=] { std::cout << name << '\n'; }; // OK
auto l3 = [&] { std::cout << name << '\n'; }; // OK
...
}
};

然而,问题是即使是传递this的值,其底层捕获的仍然是引自对象(即只有指针被拷贝)。如果lambda的生命周期超过了对象的生命周期,这就会出现问题。一个重要的例子是当用lambda为新线程定义task,它应该使用对象的拷贝来避免任何并发或者生命周期问题。另一个原因可能只是传递一个对象的副本当前状态。

C++14有一个临时的解决方案,但是它读起来不好,工作起来也不好:

1
2
3
4
5
6
7
8
9
10
class C {
private:
std::string name;
public:
...
void foo() {
auto l1 = [thisCopy=*this] { std::cout << thisCopy.name << '\n'; };
...
}
};

举个例子,就算使用=&捕获了对象,开发者仍然可能不小心用到this
1
2
3
4
auto l1 = [&, thisCopy=*this] {
thisCopy.name = "new name";
std::cout << name << '\n'; // OOPS: still the old name
};

C++17开始,你可以显式地通过*this说明你想捕获当前对象的复制:
1
2
3
4
5
6
7
8
9
10
class C {
private:
std::string name;
public:
...
void foo() {
auto l1 = [*this] { std::cout << name << '\n'; };
...
}
};

捕获*this意味着当前对象的复制传递到了lambda。

在捕获了*this的情况下你仍然可以捕获其他this,只要没有与其他的发生冲突:

1
2
auto l2 = [&, *this] { ... };     // OK
auto l3 = [this, *this] { ... }; // ERROR

这里一个完整的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// lang/lambdathis.cpp
#include <iostream>
#include <string>
#include <thread>

class Data {
private:
std::string name;
public:
Data(const std::string& s) : name(s) {
}
auto startThreadWithCopyOfThis() const {
// start and return new thread using this after 3 seconds:
using namespace std::literals;
std::thread t([*this] {
std::this_thread::sleep_for(3s);
std::cout << name << '\n';
});
return t;
}
};

int main()
{
std::thread t;
{
Data d{"c1"};
t = d.startThreadWithCopyOfThis();
} // d is no longer valid
t.join();
}

lambda用*this获取对象拷贝,即d。因此,即便是d的析构函数被调用后线程再使用传递的对象也没有问题。

如果我们使用[this],[=][&]捕获this,线程会产生未定义行为,因为在lambda打印name时,lambda使用的是已经析构后的对象的成员。

6.3 捕获引用

通过使用新的utility库函数,你现在可以捕获const对象引用

6.4 后记

constexpr最初由 Faisal Vali, Ville Voutilainen和Gabriel Dos Reis在https://wg21.link/n4487中提出。最后这个特性的公认措辞是由Faisal Vali, Jens
Maurer和Richard Smith在https://wg21.link/p0170r1中给出。

捕获*this最初由H. Carter Edwards, Christian Trott, Hal Finkel, Jim Reus, Robin Maffeo和Ben Sander在https://wg21.link/p0018r0中提出。最后这个特性的公认措辞是由 H. Carter Edwards, Daveed Vandevoorde, Christian Trott, Hal Finkel,
Jim Reus, Robin Maffeo和Ben Sander在https://wg21.link/p0180r3中给出。

第七章 新属性和属性相关特性

C++11开始,你可以指定属性(attribute,一种规范的注解,可以启用或者禁用一些warning)。C++17还引入了新的属性。此外,属性现在可以在更多的地方使用,并且有一些额外的便利。

7.1 [[nodiscard]]属性

新属性[[nodiscard]]用于鼓励编译器,当发现函数返回值没有被使用的时候,产生一个warning。

通常,这个属性可以用于通知一些返回值没有使用的错误行为。错误行为可能是:

  • 内存泄漏,比如没有使用已经分配并返回的内存
  • 不符合期望,或者非直观行为,比如没有使用返回值时候可能产生的一些不同寻常/不符合期望的行为
  • 不必要的负载,比如如果没有使用返回值,这个调用过程相当于无操作。

这是一些例子,它们展示了这个属性的是有用的:

  • 分配资源必须由另一个函数释放的函数应标记为
    [[nodiscard]]。 一个典型的例子是分配内存的函数,例如malloc()或分配器的成员函数allocate()
    但是请注意,某些函数可能会返回一个值,后续无需再针对这个值做其他调用。 例如,程序员调用大小为零字节的C函数realloc(0以释放内存,这个函数的返回值就不必保存以后再调用free()
  • 一个关于不使用返回值那么函数的行为将会改变的例子是std::async(由C++11引入)。它的目的是异步启动任务,并返回一个句柄以等待其结束(并使用结果)。当返回值没使用时,这个调用会成为同步调用,因为未使用的返回值的析构函数会立即调用,即立刻开始等待任务结束。 因此,不使用返回值会与std::async()的设计目的相矛盾。 这种情况下用[[nodiscard]]让编译器对此发出警告。
  • 另一个例子是成员函数empty(),它检查对象是否没有元素。程序员有时候可能错误的调用这个函数来清空容器(译注:即误以为empty做动词)
    1
    cont.empty();
    这种对empty()的误用可以被检查出来,因为它的返回值没有被使用。将成员函数标注这个属性即可:
    1
    2
    3
    4
    5
    6
    class MyContainer {
    ...
    public:
    [[nodiscard]] bool empty() const noexcept;
    ...
    };
    尽管这个是C++17引入的,但是标准库至今都没有使用它。对于C++17来说,应用此功能的建议来得太晚了。因此关于这个特性的关键动机,即为std::async()的声明添加现在都没有完成。对于上述所有示例,下一个C++标准将附带相应的修复程序(具体参见已经接受的提案https://wg21.link/p0600r1)。为了使代码更具可移植性,你应该使用它,而不是使用不可移植的方式(比如gcc或者clang的[[gnu:warn_unused_result]])来标注函数。当定义operator new()时你应该为函数标记[[nodiscard]]

7.2 [[maybe_unused]]属性

新属性[[maybe_unused]]可以用来避免编译器为未被使用的名字或者对象发出警告。

这个属性可以用在类声明上、类型定义typedef或者using上、变量、非静态数据成员、函数、枚举类型或者枚举值。

这个属性的一个应用是标记那些不是必要的参数:

1
2
3
4
5
6
7
void foo(int val, [[maybe_unused]] std::string msg)
{
#ifdef DEBUG
log(msg);
#endif
...
}

另一个例子是标记可能不会使用的成员
1
2
3
4
5
6
class MyStruct {
char c;
int i;
[[maybe_unused]] char makeLargerSize[100];
...
};

注意,你不能为一个语句标注[[maybe_unused]]。基于这个原因,你不能使用让[[maybe_unused]][[nodiscard]]相见:
1
2
3
4
5
6
int main()
{
foo(); // WARNING: return value not used
[[maybe_unused]] foo(); // ERROR: attribute not allowed here
[[maybe_unused]] auto x = foo(); // OK
}

7.3 [[fallthrough]]属性

新属性[[fallthrough]]可以让编译器不警告那些switch中的某个case没有break,导致其他case被相继执行的情况。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void commentPlace(int place)
{
switch (place) {
case 1:
std::cout << "very ";
[[fallthrough]];
case 2:
std::cout << "well\n";
break;
default:
std::cout << "OK\n";
break;
}
}

传递1会输出
1
very well

同时执行了case 1和case 2。

注意这个属性必须被用在空语句中。因此,你需要在它尾巴上加个分号。

在switch的最后一条语句使用这个属性是不允许的。

7.4 通用属性扩展

下面的特性在C++17zhong被启用:

  1. 现在允许为namespace标记属性。比如,你可以像下面代码一样弃用一个命名空间:
    1
    2
    3
    namespace [[deprecated]] DraftAPI {
    ...
    }
    也可以用于inline namespace和匿名namespace。
  2. 枚举值现在也可以标注属性。

比如,你可以引入新的枚举值代替原有的枚举值,然后弃用原有枚举值:

1
2
3
4
enum class City { Berlin = 0,
NewYork = 1,
Mumbai = 2, Bombay [[deprecated]] = Mumbai,
... };

Mumbai和Bombay都表示相同的city数值,但是Bombay已经弃用。注意标记枚举值时,语法上需要将属性放到枚举值名字的后面。

  1. 用户定义的属性它们通常在自己的namespace定义,你现在可以使用using来避免重复书写namespace。换句话说,以前写法是:
    1
    [[MyLib::WebService, MyLib::RestService, MyLib::doc("html")]] void foo();
    现在你可以这么写:
    1
    [[using MyLib: WebService, RestService, doc("html")]] void foo();
    注意用了using之后再书写namespace前缀会出错的:
    1
    [[using MyLib: MyLib::doc("html")]] void foo(); // ERROR

7.5 后记

这三个属性最初由Andrew Tomazos在https://wg21.link/p0068r0中提出。最后[[nodiscard]]的公认措辞是由Andrew Tomazos在https://wg21.link/p0189r1中给出。[[maybe_unused]]的公认措辞是由Andrew Tomazos在https://wg21.link/p0212r1中给出。[[fallthrough]]的公认措辞是由Andrew Tomazos在https://wg21.link/p0188r1中给出。

允许namespace和枚举值标注属性这个特性最初由 Richard Smith在https://wg21.link/n4196中提出。最后的公认措辞是由 Richard Smith在https://wg21.link/n4266中给出。

属性允许使用using这个特性最初由J. Daniel Garcia, Luis M. Sanchez, Massimo
Torquati, Marco Danelutto和Peter Sommerlad在https://wg21.link/p0028r0中提出。最后的公认措辞是由J. Daniel Garcia and Daveed Vandevoorde在https://wg21.link/P0028R4中给出。

第八章 其他语言特性

有一些小的C++核心语言特性改动,它们会在本章描述。

8.1 嵌套命名空间

最早这个提案是在2003年提出的,C++标准委员会现在终于最终接受了它:

1
2
3
namespace A::B::C {
...
}

它等价于:
1
2
3
4
5
6
7
namespace A {
namespace B {
namespace C {
...
}
}
}

嵌套的inline命名空间还不支持。这是因为如果用了inline就不知道到底inline是针对最后一个还是对所有命名空间使用。

8.2 定于表达式求值顺序

很多代码库和C++书籍包含的代码首先给出符合直觉的假设,然后代码上看起来是有效的,但是严格来讲,这些代码可能产生未定义行为。一个例子是使用寻找并替换子字符串:

1
2
3
std::string s = "I heard it even works if you don't believe";
s.replace(0,8,"").replace(s.find("even"),4,"sometimes")
.replace(s.find("you don✬t"),9,"I");

直觉上看起来这段代码是有效的,它将前8个字符替换为空,“even”替换为“sometimes”,将“you don’t”替换为“I”:
1
it sometimes works if I believe

然而,在C++17之前,结果是不保证的,因为,虽然find()调用返回从何处开始替换,但是当整个语句执行并且在结果被需要之前,这个调用可能在任何时候执行。实际上,所有find(),即计算待替换的起始索引,都可能在任何替换发生前被执行,因此结果是:
1
it sometimes works if I believe

其他结果也是可能的:
1
2
3
it sometimes workIdon’t believe
it even worsometiIdon’t believe
it even worsometimesf youIlieve

另一个例子是使用输出运算符来打印计算后的表达式的值:
1
std::cout << f() << g() << h();

通常的假设是f()g()之前被调用,两者又都在h()之前被调用。然而,这个假设是错误的。f()g()h()可以按任意顺序调用,这可能导致一些奇怪的,甚至是糟糕的结果,尤其是当这些调用互相依赖时

具体来说,考虑下面的例子,在C++17之前,这段代码会产生未定义行为:

1
2
i = 0;
std::cout << ++i << ' ' << --i << '\n';

在C++17之前,他可能输出1 0,也可能输出0 -1,甚至是0 0。不管i是int还是用户定义的类型,都可能这样。(对于基本类型,一些编译器至少会warning这个问题)。

要修复这个未定义行为,一些运算符/操作符的求值被挑战,因此现在它们有确定的求值顺序:

  • 对于
    • e1 [ e2 ]
    • e1 . e2
    • e1 .* e2
    • e1 ->* e2
    • e1 << e2
    • e1 >> e2
      e1保证在e2之前求值,它们的求值顺序是从左至右。

然而,相同函数的不同实参的求值顺序仍然是未定义的。即:

1
e1.f(a1,a2,a3)

e1保证在a1 a2 a3之前求值。但是a1 a2 a3的求职顺序仍然是未定义的。

  • 所有赋值运算符
    • e2 = e1
    • e2 += e1
    • e2 *= e1
    • ...
      右手边的e1会先于左手变的e2被求值。
  • 最后,new表达式中
    • new Type(e)
      分配行为保证在e之前求值,初始化新的值保证在任何使用初始化的值之前被求值。

上述所有保证对基本类型和用户定义类型都有效。

这样做的效果是,C++17后:

1
2
3
std::string s = "I heard it even works if you don't believe";
s.replace(0,8,"").replace(s.find("even"),4,"sometimes")
.replace(s.find("you don✬t"),9,"I");

保证会改变s的值,变成:
1
it always works if you use C++17

因此,每个find()之前的替换都会在find()之前被求值。

另一个结果是,下面的语句

1
2
i = 0;
std::cout << ++i << ' ' << --i << '\n';

其输出保证是1 0

然而,对于其他大多数运算符而言,求值顺序仍然未定义。举个例子:

1
i = i++ + i; // still undefined behavior

这里右手变的i可能在递增之前或者递增之后传递给左手变。

另一个使用new表达式求值顺序的例子是在传值之前插入空格的函数

向后兼容

新的求值顺序的保证可能影响既有程序的输出。这不是理论上可能,是真的。考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>

void print10elems(const std::vector<int>& v)
{
for (int i=0; i<10; ++i) {
std::cout << "value: " << v.at(i) << '\n';
}
}

int main()
{
try {
std::vector<int> vec{7, 14, 21, 28};
print10elems(vec);
}
catch (const std::exception& e) { // handle standard exception
std::cerr << "EXCEPTION: " << e.what() << '\n'; }
catch (...) { // handle any other exception
std::cerr << "EXCEPTION of unknown type\n";
}
}

因为这里的vector<>只有4个元素,程序会在print10elems()的循环中,调用at()时遇到无效索引抛出异常:
1
std::cout << "value: " << v.at(i) << "\n";

在C++17之前,可能输出:
1
2
3
4
5
value: 7
value: 14
value: 21
value: 28
EXCEPTION: ...

因为at()可以在”value “输出之前求值,所以对于错误的索引可能直接跳过不输出”value “。

自C++17之后,保证输出:

1
2
3
4
5
value: 7
value: 14
value: 21
value: 28
value: EXCEPTION: ...

因为”value “一定在at()调用之前执行。

8.3 宽松的基于整数的枚举初始化

对于有固定基本类型的枚举,C++17允许你使用带数值的列表初始化。

1
2
3
4
5
6
7
8
9
10
11
12
// unscoped enum with underlying type:
enum MyInt : char { };
MyInt i1{42}; // C++17 OK (C++17之前错误)
MyInt i2 = 42; // 仍然错误
MyInt i3(42); // 仍然错误
MyInt i4 = {42}; // 仍然错误

enum class Weekday { mon, tue, wed, thu, fri, sat, sun };
Weekday s1{0}; // C++17 OK (C++17之前错误)
Weekday s2 = 0; // 仍然错误
Weekday s3(0); // 仍然错误
Weekday s4 = {0}; // 仍然错误

类似的,如果Weekday有基本类型:
1
2
3
4
5
6
// scoped enum with specified underlying type:
enum class Weekday : char { mon, tue, wed, thu, fri, sat, sun };
Weekday s1{0}; // C++17 OK (C++17之前错误)
Weekday s2 = 0; // 仍然错误
Weekday s3(0); // 仍然错误
Weekday s4 = {0}; // 仍然错误

对于没有指定基本类型的未限域枚举(不带class的enum),你仍然不能使用带数值的列表初始化:
1
2
enum Flag { bit1=1, bit2=2, bit3=4 };
Flag f1{0}; // 仍然错误

注意,列表初始化还是不允许变窄(narrowing),因此你不能传递浮点值:
1
2
enum MyInt : char { };
MyInt i5{42.2}; // 仍然错误

之所以提出这个特性,是想实现一种技巧,即基于原有的整数类型定义另一种新的枚举类型,就像上面MyInt一样。

实际上,C++17的标准库中的std::byte也提供这个功能,它直接使用了这个特性。

8.4 修复带auto和直接列表初始化一起使用产生的矛盾行为

C++11引入了统一初始化后,结果证明它和auto搭配会不幸地产生反直觉的矛盾行为:

1
2
3
4
int x{42};      // initializes an int
int y{1,2,3}; // ERROR
auto a{42}; // initializes a std::initializer_list<int>
auto b{1,2,3}; // OK: initializes a std::initializer_list<int>

这些使用直接列表初始化(direct list initialization,不带=的花括号)造成的前后不一致行为已经得到修复,现在程序行为如下:
1
2
3
4
int x{42};      // initializes an int
int y{1,2,3}; // ERROR
auto a{42}; // initializes an int now
auto b{1,2,3}; // ERROR now

注意这是一个非常大的改变,甚至可能悄悄的改变程序的行为。出于这个原因,编译器接受这个改变,但是通常也提供C++11版本的模式。对于主流编译器,比如Visual Studio 2015,g++5和clang3.8同时接受两种模式。

还请注意拷贝列表初始化(copy list initialization,带=的花括号)的行为是不变的,当使用auto时初始化一个std::initializer_list<>

1
2
auto c = {42}; // still initializes a std::initializer_list<int>
auto d = {1,2,3}; // still OK: initializes a std::initializer_list<int>

因此,现在的直接列表初始化(不带=)和拷贝列表初始化(带=)有另一个显著区别:
1
2
auto a{42}; // initializes an int now
auto c = {42}; // still initializes a std::initializer_list<int>

推荐的方式是总是使用直接列表初始化(不带=的花括号)来初始化变量和对象。

8.5 十六进制浮点字面值

C++17标准化了十六进制的浮点值字面值(有些编译器早已在C++17之前就支持了)。这种方式尤其适用于要求精确的浮点表示(对于双精度浮点值,没法保证精确值的存在)。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// lang/hexfloat.cpp
#include <iostream>
#include <iomanip>

int main() {
// init list of floating-point values:
std::initializer_list<double> values{
0x1p4, // 16
0xA, // 10
0xAp2, // 40
5e0, // 5
0x1.4p+2, // 5
1e5, // 100000
0x1.86Ap+16, // 100000
0xC.68p+2, // 49.625
};

// print all values both as decimal and hexadecimal value:
for (double d : values) {
std::cout << "dec: " << std::setw(6) << std::defaultfloat << d
<< " hex: " << std::hexfloat << d << '\n';
}
}

这个程序使用不同的方式定义了不同的浮点值,其中包括使用十六进制浮点记法。新的记法是base为2的科学表示法:

  • significant/mantissa写作十六进制方式
  • exponent写作数值方式,解释为base为2

比如说,0xAp2是指定数值40(10乘以2的次方)。这个值也可以表示为0x1.4p+5,表示1.25乘以32(0.4是十六进制的四分之一,2的5次方是32)。

程序输出如下:

1
2
3
4
5
6
7
8
dec: 16     hex: 0x1p+4
dec: 10 hex: 0x1.4p+3
dec: 40 hex: 0x1.4p+5
dec: 5 hex: 0x1.4p+2
dec: 5 hex: 0x1.4p+2
dec: 100000 hex: 0x1.86ap+16
dec: 100000 hex: 0x1.86ap+16
dec: 49.625 hex: 0x1.8dp+5

如你说见,这个例子的浮点记法早已在C++11的std::hexfloat操作符上就已经支持了。

8.6 UTF-8字符串字面值

C++11支持以u8前缀表示的UTF-8字符串字面值。然而,这个前缀对于字符是不支持的。C++17修复了这个问题,你现在可以这样写:

1
char c = u8'6'; // character 6 with UTF-8 encoding value

样可以保证字符值是UTF-8中字符‘6’的值。你可以使用所有的7bits US-ASCII字符,对于这些字符,UTF-8代码具有相同的值。换句话说,用这个指定的值和US-ASCII、ISO Latin-1、ISO-8859-15和基本Windows字符集的值都是一样的。通常,你的源代码的字符都会被解释为US-ASCII/UTF-8,所以前缀不是很重要。变量c的值几乎总是54(十六进制的36)。

对于源码中的字符和字符串字面值,C++标准化了你可以使用哪些字符,但是没有标准化这些字符对应的值。这些值取决于源代码字符集。当编译器生成可执行程序时,它会使用运行时字符集。源代码字符集集合总是7bits的US-ASCII,并且运行时字符集通常和源代码字符集一样。对于任何C++程序,有没有u8前缀这些字符和字符串字面值都是一样的。但是在很少见的情况下,可能不是这样。比如老式的IBM主机,仍然使用EBCDIC字符集,在这个字符集中字符‘6’的值是246(十六进制F6)。如果程序使用EBCDIC字符集,那么c的值将会是246而不是54,并且在UTF-8编码的平台上运行该程序时可能输出”¨o”,因为它对应ASCII值的246.在这种情况下前缀可能是必要的。

注意u8只能用于单个字符和UTF-8单字节字符。下面的初始化:

是不被允许的,因为这个德语字符在UTF-8是双字节,即195和182(十六进制C3 B6)。

总结来熟哦,所有允许的字符和字符串字面值如下:

  • 单字节US-ASCII和UTF-8可以使用u8
  • 双字节的UTF-16可以使用u
  • 四字节的UTF-32可以使用U
  • 没有指定编码的宽字符可以使用l,它可能是两字节也可能是四字节

8.7 异常声明成为类型的一部分

C++17开始异常处理声明成为一个函数的类型的一部分。也就是说,下面的两个函数现在有不同的类型:

1
2
void f1();
void f2() noexcept; // different type

在C++17之前,这两个函数的类型是相同的。

这样的后果是,现在的编译器会检查是否你将不抛异常的函数传递给抛异常的函数指针:

1
2
3
void (*fp)() noexcept;  // pointer to function that doesn’t throw
fp = f2; // OK
fp = f1; // ERROR since C++17

给抛异常的函数指针传递不抛异常的函数仍然有效:
1
2
3
void (*fp2)();  // pointer to function that might throw
fp2 = f2; // OK
fp2 = f1; // OK

所以,这个新的特性不会破坏哪些没有使用noexcept作为函数指针的一部分的那些程序。

异常声明有无不能作为重载函数的依据:

1
2
void f3();
void f3() noexcept; // ERROR

注意,其他规则是不受影响的。举个例子,下面的代码中你还是不能忽略基类noexcept声明:
1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
virtual void foo() noexcept;
...
};

class Derived : public Base {
public:
void foo() override; // ERROR: does not override
...
};

子类的foo()的类型与基类的foo()类型不一致,所以不允许重载,这个代码不能通过编译。即便没有指定override修饰符,还是不能编译,因为我们不能用更宽松的抛异常的版本来重载不抛异常的严格版本。

使用条件异常声明

当使用条件异常声明时,函数的类型取决于条件为true还是false:

1
2
3
4
void f1();
void f2() noexcept;
void f3() noexcept(sizeof(int)<4); // same type as either f1() or f2()
void f4() noexcept(sizeof(int)>=4); // different type than f3()

在这里,当代码编译时f3()的类型取决于条件:

  • 如果sizeof(int)为4(或者更多),最终的签名是
    1
    void f3() noexcept(false);    // same type as f1()
  • 如果sizeof(int)小于4,最终签名是:
    1
    void f3() noexcept(true);     // same type as f2()
    因为f4()的异常条件与f3()相反,所以f4()的类型总是与f3()不一样(即保证f3()抛异常它就不抛,f3()不抛它就抛)。

老式的空异常声明仍然可以使用,但是C++17已经标为废弃:

1
void f5() throw(); // same as void f5() noexcept but deprecated

动态的异常声明已经不再支持(它们在C++11时已经标为废弃):
1
void f6() throw(std::bad_alloc); // ERROR: invalid since C++17

对泛型库的影响

让noexcept成为类型的一部分可能对一些泛型库造成影响。

比如,下面的程序截止C++14是有效的,但是在C++17中无法编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// lang/noexceptcalls.cpp
#include <iostream>

template<typename T>
void call(T op1, T op2)
{
op1();
op2();
}

void f1() {
std::cout << "f1()\n";
}

void f2() noexcept {
std::cout << "f2()\n";
}

int main()
{
call(f1, f2); // ERROR since C++17
}

原因是C++17中f1()f2()的类型不一样,编译器在实例化模板调用call()的时候不能为两个类型找到相同的类型T。

在C++17下,你不得不用两个类型:

1
2
3
4
5
6
7
8
9
10
11
template<typename T1, typename T2>
void call(T1 op1, T2 op2)
{
op1();
op2();
}
````
如果你想,或者不得不重载所有可能的函数类型,你需要付出双倍。来看`std::is_function<>`,主要的函数模板定义如下,通常T不是函数:
```cpp
// primary template (in general type T is no function):
template<typename T> struct is_function : std::false_type { };

这个模板继承自std::false_type,所以is_function<T>::value通常产生false。

对于那些的确是函数的类型,需要偏特化,它继承自std::true_type,所以成员value的值是true:

1
2
3
4
5
6
7
8
9
// partial specializations for all function types:
template<typename Ret, typename... Params>
struct is_function<Ret (Params...)> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) &> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const &> : std::true_type { };

C++17之前,它已经有24个偏特化来,因为函数可能有const和volatile修饰符,也可能有lvalue和rvalue引用修饰符,你重载的函数需要可变参数模板类型。

C++17后,偏特化的数量将会翻倍,因为有了新的noexcept修饰符,所以现在有48个:

1
2
3
4
5
6
7
8
9
10
...
// partial specializations for all function types with noexcept:
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) noexcept> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const noexcept> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) & noexcept> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const& noexcept> : std::true_type { };

没有实现noexcept重载的库可能编译不了一些代码,因为它们可能用了noexcept。

8.8 单参数的static_assert

C++17开始,之前static_assert()必须传的错误消息参数现在变成可选了。这意味着最后的诊断性消息完全平台特定。比如:

1
2
3
4
5
6
7
8
9
10
11
#include <type_traits>

template<typename T>
class C {
// OK since C++11:
static_assert(std::is_default_constructible<T>::value,
"class C: elements must be default-constructible");
// OK since C++17:
static_assert(std::is_default_constructible_v<T>);
...
};

没有传消息的断言使用了新的type trait后缀_v

8.9 预处理条件__has_include

C++17扩展了预处理起,可以检查一个特定的头文件是否被include。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#if __has_include(<filesystem>)
# include <filesystem>
# define HAS_FILESYSTEM 1
#elif __has_include(<experimental/filesystem>)
# include <experimental/filesystem>
# define HAS_FILESYSTEM 1
# define FILESYSTEM_IS_EXPERIMENTAL 1
#elif __has_include("filesystem.hpp") # include "filesystem.hpp" # define HAS_FILESYSTEM 1
# define FILESYSTEM_IS_EXPERIMENTAL 1
#else
# define HAS_FILESYSTEM #if __has_include(<filesystem>)
# include <filesystem>
# define HAS_FILESYSTEM 1
#elif __has_include(<experimental/filesystem>)
# include <experimental/filesystem>
# define HAS_FILESYSTEM 1
# define FILESYSTEM_IS_EXPERIMENTAL 1
#elif __has_include("filesystem.hpp") # include "filesystem.hpp" # define HAS_FILESYSTEM 1
# define FILESYSTEM_IS_EXPERIMENTAL 1
#else
# define HAS_FILESYSTEM 0
#endif0
#endif

如果#include成功则__has_include(...)会求值为1(true)。如果不成功则没有什么影响。

8.10 后记

嵌套namespace定义最初由Jon Jagger在2003年于https://wg21.link/n1524提出。Robert Kawulak在2014年于https://wg21.link/n4026提出了新的提案。最后这个特性的公认措辞是由Robert Kawulak 和 Andrew Tomazos在https://wg21.link/n4230中给出。

重新定义后的求值顺序最初由Gabriel Dos Reis, Herb Sutter和Jonathan Caves在https://wg21.link/n4228中提出。最后这个特性的公认措辞是由Gabriel Dos Reis, Herb Sutter和Jonathan Caves在https://wg21.link/p0145r3中给出。

更宽松的枚举初始化最初由Gabriel Dos Reis在https://wg21.link/p0138r0中提出。最后这个特性的公认措辞是由Gabriel Dos Reis在https://wg21.link/p0138r2中给出。

修复带auto和直接列表初始化一起使用产生的矛盾行为最初由Ville Voutilainen在 https://wg21.link/n3681https://wg21.link/3912中提出。最后这个特性的公认措辞是由 James Dennett在https://wg21.link/n3681中给出。

十六进制浮点值最初由Thomas Koppe在https://wg21.link/p0245r0中提出。最后这个特性的公认措辞是由Thomas Koppe在https://wg21.link/p0245r1中给出。

UTF-8字符串字面值最初由 Richard Smith在https://wg21.link/n4197中提出。最后这个特性的公认措辞是由 Richard Smith在https://wg21.link/n4267中给出。

异常声明成为类型的一部分最初由Jens Maurer在https://wg21.link/n4320中提出。最后这个特性的公认措辞是由Jens Maurer在https://wg21.link/p0012r1中给出。

单参数的static_assert的公认措辞是由Walter E. Brown在https://wg21.link/n3928中给出。

预处理条件__has_include最初由Clark Nelson和RichardSmith在https://wg21.link/p0061r0中作为其中一部分提出。最后这个特性的公认措辞是由Clark Nelson和RichardSmith在https://wg21.link/p0061r1中给出。

第九章 类模板参数推导

C++17之前,你必须显式指定类模板的所有模板参数类型。比如,你不能忽略这里的double:

1
std::complex<double> c{5.1,3.3};

也不能忽略第二次的std::mutex
1
2
std::mutex mx;
std::lock_guard<std::mutex> lg(mx);

C++17开始,必须显式指定类模板的所有模板参数类型这个限制变得宽松了。有了类模板参数推导(class template argument deduction,CTAD)技术,如果构造函数可以推导出所有模板参数,那么你可以跳过显式指定模板实参。

比如:

  • 你可以这样声明:
    1
    std::complex c{5.1,3.3}; // OK: std::complex<double> deduced
  • 你可以这样实现:
    1
    2
    std::mutex mx;
    std::lock_guard lg{mx}; // OK: std::lock_guard<std_mutex> deduced
  • 你甚至可以让容器推导其元素的类型:
    1
    2
    std::vector v1 {1, 2, 3} // OK: std::vector<int> deduced
    std::vector v2 {"hello", "world"}; // OK: std::vector<const char*> deduced

9.1 使用类模板参数推导

只要传给构造函数的实参可以用来推导类型模板参数,那么就可以使用类模板参数推导技术。该技术支持所有初始化方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::complex c1{1.1, 2.2}; // deduces std::complex<double>
std::complex c2(2.2, 3.3); // deduces std::complex<double>
std::complex c3 = 3.3; // deduces std::complex<double>
std::complex c4 = {4.4}; // deduces std::complex<double>
````
c3和c4的初始化方式是可行的,因为你可以传递一个值来初始化`std::complex<>`,这对于推导出模板参数T来说足够了,它会被用于实数和虚数部分:
```cpp
namespace std {
template<typename T>
class complex {
constexpr complex(const T& re = T(), const T& im = T());
...
}
};

假设有如下声明
1
std::complex c1{1.1, 2.2};

编译器会在调用的地方找到构造函数
1
constexpr complex(const T& re = T(), const T& im = T());

因为两个参数T都是double,所以编译器推导出T是double,然后编译下面的代码:
1
2
complex<double>::complex(const double& re = double(),
const double& im = double());

注意模板参数必须是无歧义、可推导的。因此,下面的初始化是有问题的:
1
std::complex c5{5,3.3}; // ERROR: attempts to int and double as T

对于模板来说,不会在推导模板参数的时候做类型转换。

对于可变参数模板的类模板参数推导也是支持的。比如,std::tuple<>定义如下:

1
2
3
4
5
6
7
8
namespace std {
template<typename... Types>
class tuple;
public:
constexpr tuple(const Types&...);
...
};
};

这个声明:
1
std::tuple t{42, 'x', nullptr};

推导出的类型是std::tuple<int, char, std::nullptr_t>

你也可以推导出非类型模板参数。举个例子,像下面例子中传递一个数组,在推导模板参数的时候可以同时推导出元素类型和数组大小:

1
2
3
4
5
6
7
8
template<typename T, int SZ>
class MyClass {
public:
MyClass (T(&)[SZ]) {
...
}
};
MyClass mc("hello"); // deduces T as const char and SZ as 6

SZ推导为6,因为模板参数类型传递了一个六个字符的字符串字面值。

你甚至可以推导出用作基类的lambda的类型,或者推导出auto模板参数类型。

9.1.1 默认拷贝

如果类模板参数推导发现一个行为更像是拷贝初始化,它就倾向于这么认为。比如,在用一个元素初始化std::vector后:

1
std::vector v1{42}; // vector<int> with one element

用这个vector去初始化另一个vector:
1
std::vector v2{v1}; // v2 also is vector<int>

v2会被解释为vector<int>而不是vector<vector<int>>

又比如,这个规则适用于下面所有初始化形式:

1
2
3
std::vector v3(v1); // v3 also is vector<int>
std::vector v4 = {v1}; // v4 also is vector<int>
auto v5 = std::vector{v1}; // v5 also is vector<int>

如果传递多个元素时,就不能被解释为拷贝初始化,此时initializer list的类型会成为新vector的元素类型:
1
std::vector vv{v, v}; // vv is vector<vector<int>>

那么问题来了,如果传递可变参数模板,那么类模板参数推导会发生什么:
1
2
3
4
5
6
7
8
template<typename... Args>
auto make_vector(const Args&... elems) {
return std::vector{elems...};
}

std::vector<int> v{1, 2, 3};
auto x1 = make_vector(v, v); // vector<vector<int>>
auto x2 = make_vector(v); // vector<int> or vector<vector<int>> ?

当前,不同的编译器有不同的处理方式,这个问题还在讨论中。

9.1.2 推导lambda的类型

有了类模板参数推导,我们现在终于可以用lambda的类型实例化类模板类。举个例子,我们可以提供一个泛型类,然后包装一下callback,并统计调用了多少次callback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// tmpl/classarglambda.hpp
#include <utility> // for std::forward()

template<typename CB>
class CountCalls
{
private:
CB callback; // callback to call
long calls = 0; // counter for calls
public:
CountCalls(CB cb) : callback(cb) {
}
template<typename... Args>
auto operator() (Args&&... args) {
++calls;
return callback(std::forward<Args>(args)...);
}
long count() const {
return calls;
}
};

这里,构造函数接受一个callback,然后包装一下,用它的类型来推导出模板参数CB。比如,我们可以传一个lambda:
1
2
3
CountCalls sc([](auto x, auto y) {
return x > y;
});

这意味着sc的类型被推导为CountCalls<TypeOfTheLambda>

通过这种方式,我们可以计算传递给排序函数的sc的调用次数:

1
2
3
std::sort(v.begin(), v.end(),
td::ref(sc));
std::cout << "sorted with " << sc.count() << " calls\n";

包装后的lambda通过引用的方式传递给排序函数,因为如若不然std::sort()只会计算传递给他的lambda的拷贝的调用,毕竟是传值的方式。

然而,我没可以传递包装后的lambda给std::for_each,因为这个算法可以返回传递给他的callback的拷贝:

1
2
3
4
5
auto fo = std::for_each(v.begin(), v.end(),
CountCalls([](auto i) {
std::cout << "elem: " << i << '\n';
}));
std::cout << "output with " << fo.count() << " calls\n";

9.1.3 非部分类模板参数推导

不像函数模板那样,类模板参数不能部分推导(显示模板参数的一部分)。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T1, typename T2, typename T3 = T2>
class C {
public:
C (T1 x = T1{}, T2 y = T2{}, T3 z = T3{}) {
...
}
...
};
// all deduced:
C c1(22, 44.3, "hi"); // OK: T1 is int, T2 is double, T3 is const char*
C c2(22, 44.3); // OK: T1 is int, T2 and T3 are double
C c3("hi", "guy"); // OK: T1, T2, and T3 are const char*
// only some deduced:
C<string> c4("hi", "my"); // ERROR: only T1 explicitly defined
C<> c5(22, 44.3); // ERROR: neither T1 not T2 explicitly defined
C<> c6(22, 44.3, 42); // ERROR: neither T1 nor T2 explicitly defined
// all specified:
C<string,string,int> c7; // OK: T1,T2 are string, T3 is int
C<int,string> c8(52, "my"); // OK: T1 is int,T2 and T3 are strings
C<string,string> c9("a", "b", "c"); // OK: T1,T2,T3 are strings

因为第三个模板参数类型有默认值,所以如果已经指定了第二个就可以省略第三个。

如果i想知道为什么不支持偏特化,下面是造成这个抉择的原因:

1
std::tuple<int> t(42, 43); // still ERROR

std::tuple是一个可变参数模板,所以你可以指定任意数量的参数。在这种情况下,到底是认为这是只指定了一个类型的而导致的错误还是有意为之很难说清。看起来是有问题的。后期有更多考量后,偏特化也有可能加入C++标准。尽管目前没有。

不幸的是,缺少部分特化就不能解决一个常见代码需求。对于关联容器的排序规则,或者无序容器的hash函数,我们仍然不能简单的传一个lambda:

1
2
3
std::set<Cust> coll([](const Cust& x, const Cust& y) { // still ERROR
return x.name() > y.name();
});

我们还是得指定lambda的类型,因此需要像下面这样写:
1
2
3
4
auto sortcrit = [](const Cust& x, const Cust& y) {
return x.name() > y.name();
};
std::set<Cust, decltype(sortcrit)> coll(sortcrit); // OK

9.1.4 类模板参数推导代替便捷的工具函数。

有了类模板参数推导,我们可以不再使用那些目的仅是推导传的参数的类型的便捷工具函数。

最明显的是make_pair,他允许我们不指定传的参数的类型。比如,对于v:

1
std::vector<int> v;

我们可以使用
1
auto p = std::make_pair(v.begin(), v.end());

来代替
1
std::pair<typename std::vector<int>::iterator,typename std::vector<int>::iterator> p(v.begin(), v.end());

现在,make_pair()不再需要了,可以直接这么写:
1
std::pair p(v.begin(), v.end());

第十一章 折叠表达式

自C++17起, 其特性有支持带一个(可带有初始值的)参数包(parameter pack)的所有实参能使用二元操作符并计算结果.

例如, 下列的函数能返回所有传入实参的和:

1
2
3
4
template <typename ...T>
auto foldSum(T... args) {
return (... + args); // ((arg1 + arg2) + arg3)...
}

注意, return 表达式里的括号是折叠表达式的一部分并且不能省略.

函数调用 foldSum(47, 11, val, -1); 使模版实例化并执行: return 47 + 11 + val + -1;.

函数调用 foldSum(std::string("hello"), "world", "!"); 使模版实例化为: return std::string("hello") + "world" + "!";

还要注意, 折叠表达式实参的次序可以不同并且效果也不一样 (可能看起有点反直觉): 例如写成 (... + args) 的结果则是 ((arg1 + arg2) + arg3)..., 该含义是重复地“往后添加”(post-adds)东西. 你也可以写成 (args + ...), 该含义是重复地“往前添加”(pre-adds)东西, 因此其结果为: (arg1 + (arg2 + arg3))....

11.1 折叠表达式的目的

折叠表达式避免了需要递归地去实例化模版并作用于一个参数包的所有形参. 在 C++17 之前, 你必须这样实现:

1
2
3
4
5
6
7
8
template <typename T>
auto foldSumRec(T arg) {
return arg;
}
template <typename T1, typename ...Ts>
auto foldSumRec(T1 arg1, Ts... otherArgs) {
return arg1 + foldSumRec(otherArgs...);
}

这样的一种实现不仅写起来繁琐, 并且它也给 C++ 编译器造成负担. 使用

1
2
3
4
template <typename ...T>
auto foldSum(T... args) {
return (... + args); // ((arg1 + arg2) + arg3)...
}

对于程序员和编译器双方的工作明显有所减少.

11.2 折叠表达式的使用

给定形参 args 和一个操作符 op, C++17 允许我们写成

  • 要么是一元左折叠(unary left fold)
    ( ... op args), 它将展开为: (...(arg1 op arg2) op ... argN-1) op argN)
  • 要么是一元右折叠(unary right fold)
    (args op ...), 它将展开为: (arg1 op (arg2 op ... (argN-1 op argN)...)

其中括号是必需的. 但是, 括号和省略号 (…) 不必用空格隔开.

比起知道左和右折叠表达式的预期结果, 理解两者的差别更重要. 例如, 甚至在使用 + 操作符时就有可能出现不同的效果. 在使用左折叠表达式时:

1
2
3
4
template <typename ...T>
auto foldSumL(T... args) {
return (... + args); // ((arg1 + arg2) + arg3)...
}

调用 foldSumL(1, 2, 3) 则计算出 ((1 + 2) + 3). 这也意味着下列示例代码是能被编译的:

1
std::cout << foldSumL(std::string("hello"), "world", "!") << "\n"; // 编译通过.

记住操作符 + 用于标准字符串类型则至少有一个操作数是 std::string 类型. 因为使用了左折叠表达式, 则函数第一次调用将计算 std::string("hello") + "world", 其返回结果为一个 std::string 类型的字符串, 因此再加上字面形式的字符串 "!" 也是有效的.

然而, 以下的函数调用:

1
std::cout << foldSumL("hello", "world", std::string("!")) << "\n"; // 编译报错.

将不能被编译, 因为其计算得到 (("hello" + "world") + std::string("!")), 而两个字面形式的字符串是不允许用操作符 + 进行拼接的.

然而, 我们可以将实现改成:

1
2
3
4
template <typename ...T>
auto foldSumL(T... args) {
return (args + ...); // (arg1 + (arg2 + arg3))...
}

调用 foldSumL(1, 2, 3) 则计算出 (1 + (2 + 3)). 这意味着下列示例代码就不再能被编译:

1
std::cout << foldSumL(std::string("hello"), "world", "!") << "\n"; // 编译报错.

而以下的函数调用现在能被编译:

1
std::cout << foldSumL("hello", "world", std::string("!")) << "\n"; // 编译通过.

因为几乎在所有情况下, 计算的次序都是从左至右, 通常, 参数包的左折叠语法(参数在末尾)应该更受青睐(除非它没有作用):

1
(... + args); // 更受青睐的折叠表达式语法

11.2.1 空参数包的处理

如果一个折叠表达式使用了空参数包, 则应用以下规则:

  • 如果使用了操作符 &&, 则其值为 true.
  • 如果使用了操作符 ||, 则其值为 false.
  • 如果使用了操作符 ,, 则其值是 void().
  • 其他操作符的调用则是不良形式 (ill-formed).

对于所有其他情况 (一般而言) 你可以添加一个初始值: 给定一个参数包 args, 一个初始值 value 和一个操作符 op, C++17 也允许我们写成:

  • 要么一个二元左折叠(binary left fold)
    (value op ... op args), 它将展开为: ((...((value op arg1) op arg2) op ... op argN-1) op argN)
    — 要么一个二元右折叠(binary right fold)
    (args op ... op value), 它将展开为: (arg1 op (arg2 op ... op (argN-1 op (argN op value))...))

在省略号两边的操作符 op 必须相同.

例如, 下列定义允许传递一个空参数包

1
2
3
4
template <typename ...T>
auto foldSum(T... s) {
return (0 + ... + s); // sizeof...(s) == 0 的情况也可行
}

在概念上, 不论我们添加 0 作为首个操作数或最后一个操作数应该都无所谓.

1
2
3
4
template <typename ...T>
auto foldSum(T... s) {
return (s + ... + 0); // sizeof...(s) == 0 的情况也可行
}

但对于一元折叠表达式其不同的计算次序则比预期结果更重要, 而二元左折叠表达式则更受青睐:

1
(value + ... + args); // 更受青睐的二元折叠表达式语法

还有, 首个操作数可能是特别的, 比如这个例子:

1
2
3
4
5
template <typename ...T>
void print(const T&... args)
{
(std::cout << ... << args) << "\n";
}

这里, 重要的是首次调用是传递给 print() 的第一个实参的输出, 其返回的输出流作用于其它输出的调用. 其它实现可能无法编译甚至得到发生无法预料的事情. 例如, 使用

1
std::cout << (args << ... << "\n");

调用print(1) 将编译通过但打印出的值 1 会向左移10位 ('\n' 的值通常为 10), 因此输出的结果为 1024.

注意, 在这个例子 print() 中没有空格分隔参数包的各个元素. 这样的调用 print("hello", 42, "world") 将会打印 hello42world.

为了用空格将传入的元素分隔开, 你需要一个helper函数以确保除了第一个实参之外在打印前加上空格. 例如, 用以下 helper 函数模版 spaceBefore() 可以办到:

1
2
3
4
5
6
7
8
9
10
11
12
// tmpl/addspace.hpp
template <typename T>
const T& spaceBefore(const T& arg) {
std::cout << ' ';
return arg;
}

template <typename First, typename... Args>
void print(const First& firstarg, const Args&... args) {
std::cout << firstarg;
(std::cout << ... << spaceBefore(args)) << '\n';
}

这里, (std::cout << ... << spaceBefore(args)) 这个折叠表达式展开成: (std::cout << spaceBefore(arg1) << spaceBefore(arg2) << ...)

因此, 在参数包 args 中每个元素都调用一个helper函数, 在返回被传递的实参之前打印出一个空格字符, 写入输出流 std::cout 里. 为了确保这不会应用到第一个实参, 我们添加了额外的首个形参并且不对其使用 spaceBefore().

注意, 参数包的输出的计算需要所有输出在左边.

我们也能在print()里面使用lambda来定义spaceBefore():

1
2
3
4
5
6
7
8
9
template <typename First, typename ...Args>
void print(const First& firstarg, const Args&... args) {
std::cout << firstarg;
auto spaceBefore = [](const auto& arg) {
std::cout << '';
return arg;
};
(std::cout << ... << spaceBefore(args)) << '\n';
}

然而, 注意 lambda 通过值返回对象, 这意味着将创建传入实参的没必要的拷贝. 避免不必要拷贝的方式是通过显式声明lambda的返回类型要为const auto&decltype(auto):

1
2
3
4
5
6
7
8
9
template <typename First, typename ...Args>
void print(const First& firstarg, const Args&... args) {
std::cout << firstarg;
auto spaceBefore = [](const auto& arg) -> const auto& {
std::cout << '';
return arg;
};
(std::cout << ... << spaceBefore(args)) << '\n';
}

如果你不能够将这些语句组合成这样一条语句, 那你用的C++就不能称为真正的C++:

1
2
3
4
5
6
7
8
template <typename First, typename ...Args>
void print(const First& firstarg, const Args& ...args) {
std::cout << firstarg;
(std::cout << ... << [](const auto& arg) -> decltype(auto) {
std::cout << ' ';
return arg;
}(args)) << '\n';
}

不过, 一种更简单实现print()的方式是使用一个lambda打印空格和实参并将其传递给一个一元折叠表达式(脚注: 感谢 Barry Revzin 提出来):

1
2
3
4
5
6
7
8
9
template <typename First, typename ...Args>
void print(First first, const Args& ...args) {
std::cout << first;
auto outWithSpace = [](const auto& arg) {
std::cout << ' ' << arg;
};
(..., outWithSpace(args));
std::cout << '\n';
}

通过使用一个额外的用auto声明的模版参数, 我们可以使print()更灵活地将字符类型的分隔符, 字符串或任意其它可打印的类型参数化.

11.2.2 已支持的操作符

除了., ->, 和 [] 这些操作符之外, 你可以使用所有二元操作符作用于折叠表达式.

折叠的函数调用

折叠表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// tmpl/foldcalls.cpp
#include <iostream>

// 可变数目的基类模版
template <typename ...Bases>
class MultiBase : private Bases...
{
public:
void print() {
// 调用所有基类的 print()
(..., Bases::print());
}
};

struct A {
void print() { std::cout << "A::print()\n"; }
};

struct B {
void print() { std::cout << "B::print()\n"; }
};

struct C {
void print() { std::cout << "C::print()\n"; }
};

int main()
{
MultiBase<A, B, C> mb;
mb.print();
}

这里,

1
2
3
4
5
template <typename ...Bases>
class MultiBase : private Bases...
{
...
};

允许我们用可变数目的基类初始化对象:

1
MultiBase<A, B, C> mb;

并且使用

1
(..., Base::print());

这个折叠表达式被展开为调用每一个基类的print. 这个折叠表达式展开后如下所示:

1
(A::print(), B::print(), C::print());

然而, 注意到,操作符的性质与我们使用左折叠表达式或右折叠表达式没什么关系. 这些函数总是从左往右被调用. 使用

1
(Base::print(), ...);

这个括号只是将调用组合起来, 因此第一个print()和其它两个print()的结果组合了一起如下所示:

1
A::print(), (B::print(), C::print());

但因为,操作符的计算次序总是从左向右, 仍然是在括号里面两个为一组的函数调用之前先调用第一个函数, 并且仍然是中间的函数在右边函数之前调用.

尽管如此, 这就像左表达式的结果并且能跟其计算次序匹配上, 还是建议在折叠多个函数调用时使用左折叠表达式.

组合Hash函数

一个使用,操作符组合Hash值的例子. 这个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
void hashCombine(std::size_t& seed, const T& val)
{
seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

template <typename ...Type>
std::size_t combineHashValue(const Type& ...args)
{
std::size_t seed = 0; // 初始种子
(..., hashCombine(seed, args)); // hashCombine() 调用链
return seed;
}

通过调用

1
std::size_t combinedHashValue("Hello", "World", 42);

中间的这条语句展开成:

1
(hashCombine(seed, "Hello"), hashCombine(seed, "World")), hashCombine(seed, 42));

使用这个定义, 我们可以容易地为一个某个类型的对象定义一个新的Hash函数, 例如 Customer:

1
2
3
4
5
6
struct CustomerHash
{
std::size_t operator()(const Customer& c) const {
return combineHashValue(c.getFirstname(), c.getLastname(), c.getValue());
}
};

这样我们就可以将 Customers 放入一个 std::unordered_set 的容器:

1
std::unordered_set<Customer, CustomerHash> coll;

折叠的路径遍历

你也可以使用折叠表达式去遍历一个二叉树的路径通过操作符->*:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// tmpl/foldtraverse.cpp
// 定义二叉树结构和用于遍历的helper函数.
struct Node {
int value;
Node* left;
Node* right;
Node(int i = 0) : value(i), left(nullptr), right(nullptr) {}
...
};
auto left = &Node::left;
auto right = &Node::right;

// 使用折叠表达式遍历树:
template <typename T, typename ...TP>
Node* traverse(T np, TP... paths) {
return (np ->* ... ->* paths); // np ->* path1 ->* path2 ...
}

int main()
{
// 初始二叉树的结构:
Node* root = new Node{0};
root->left = new Node{1};
root->left->right = new Node{2};
...
// 遍历二叉树:
Node* node = traverse(root, left, right);
...
}

这里,

1
(np ->* ... ->* paths)

使用一个折叠表达式从np开始去遍历可变数目的paths的元素. 当调用:

1
traverse(root, left, right);

这个折叠表达式的调用展开成:

1
root->left->right

11.2.3 使用折叠表达式作用于类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tmpl/ishomogeneous.hpp
#include <type_traits>

// 检查传递的类型是否为同一类:
template <typename T1, typename ...TN>
struct IsHomogeneous {
static constexpr bool value = (std::is_same<T1, TN>::value && ...);
};

// 检查传递的实参是否有相同类型:
template <typename T1, typename ...TN>
constexpr bool isHomogeneous(T1, TN...)
{
return (std::is_same<T1, TN>::value && ...);
}

这个类型 trait IsHomogeneous<> 可被使用如下:

1
IsHomogeneous<int, Size, decltype(42)>::value

此情况下, 这个初始化成员变量value的折叠表达式展开成:

1
std::is_same<int, MyType>::value && std::is_same<int, decltype(42)>::value

这个函数模版isHomogeneous<>() 可被使用如下:

1
isHomogeneous(43, -1, "hello", nullptr)

此情况下, 这个初始化成员变量value的折叠表达式展开成:

1
std::is_same<int, int>::value && std::is_same<int, const char*>::value && std::is_same<int, std::nullptr_t>::value

通常, 操作符&&是短路的(第一false则终止计算).

在标准库里的std::arary<>的推导规则使用这种特性.

11.3 后记

折叠表达式最初由Andrew Sutton和Richard Smith在https://wg21.link/n4191中提出. 最后这个特性的公认措辞由Andrew Sutton和Richard Smith在https://wg21.link/n4295中制定的. Thibaut Le Jehan 在 https://wg21.link/n0036 中提出了删除对操作符*, +, &|支持空参数包的情况.