使用调度程序的 C++ NTOH 转换 - 事件队列
C++ NTOH conversion with dispatcher - event queue
我们正在将 C 语言的遗留代码重写为 C++。在我们系统的核心,我们有一个连接到 master 的 TCP 客户端。主人将不断地流式传输消息。每个套接字读取都会产生 N 条格式为 - {type, size, data[0]}
的消息。
现在我们不将这些消息复制到单独的缓冲区中 - 而是将指针传递给消息的开头、长度和 shared_ptr 到底层缓冲区给工人。
旧版 C 版本是单线程的,会像下面这样进行就地 NTOH 转换:
struct Message {
uint32_t something1;
uint16_t something2;
};
process (char *message)
Message *m = (message);
m->something1 = htonl(m->something1);
m->something2 = htons(m->something2);
然后使用消息
在登录新代码时遇到一些问题。
由于我们将消息分派给不同的工作人员,每个进行 ntoh 转换的工作人员都会导致缓存未命中问题,因为消息未在缓存中对齐 - 即没有填充 b/w留言。
同一条消息可以由不同的工作人员处理 - 这种情况下消息需要在本地处理并中继到另一个进程。这里中继工作者需要原始网络顺序的消息,本地工作需要转换为主机顺序。显然由于消息不重复,两者都不能满足。
我想到的解决方案是 -
复制该消息并发送一份给所有中继工作者(如果有的话)。在调度之前对属于同一缓冲区的所有消息在调度程序中执行 ntoh 转换 - 例如通过调用 handler->ntoh(message);
以便解决缓存未命中问题。
向每位工人发送原件。每个worker将消息复制到本地缓冲区,然后进行ntoh转换并使用它。这里每个工作人员都可以使用线程特定的 (thread_local) 静态缓冲区作为暂存器来复制消息。
现在我的问题是
选项 1 是进行 ntoh 转换的方法 - C++sy 吗?我的意思是结构的对齐要求将不同于 char 缓冲区。 (我们还没有遇到任何问题。)。在这种情况下使用方案 2 应该没问题,因为临时缓冲区可以对齐 max_align_t,因此应该可以类型转换为任何结构。但这会导致复制整个消息 - 这可能会很大(比如几 K 大小)
有更好的处理方法吗?
您的主要问题似乎是如何处理未对齐的消息。也就是说,如果每个消息结构在其末尾没有足够的填充以使后续消息正确对齐,您可以通过将指向消息开头的指针重新解释为 object 来触发未对齐的读取。
我们可以通过多种方式解决这个问题,也许最简单的方法是 ntoh
基于 single-byte 指针,它实际上总是对齐的。
我们可以隐藏包装器 classes 背后令人讨厌的细节,它将采用指向消息开头的指针并具有将 ntoh
适当字段的访问器。
如评论中所述,要求偏移量由 C++ 结构确定,因为消息最初是这样创建的,可能不会打包。
首先,我们的 ntoh
实现是模板化的,因此我们可以 select 一个类型:
template <typename R>
struct ntoh_impl;
template <>
struct ntoh_impl<uint16_t>
{
static uint16_t ntoh(uint8_t const *d)
{
return (static_cast<uint16_t>(d[0]) << 8) |
d[1];
}
};
template <>
struct ntoh_impl<uint32_t>
{
static uint32_t ntoh(uint8_t const *d)
{
return (static_cast<uint32_t>(d[0]) << 24) |
(static_cast<uint32_t>(d[1]) << 16) |
(static_cast<uint32_t>(d[2]) << 8) |
d[3];
}
};
template<>
struct ntoh_impl<uint64_t>
{
static uint64_t ntoh(uint8_t const *d)
{
return (static_cast<uint64_t>(d[0]) << 56) |
(static_cast<uint64_t>(d[1]) << 48) |
(static_cast<uint64_t>(d[2]) << 40) |
(static_cast<uint64_t>(d[3]) << 32) |
(static_cast<uint64_t>(d[4]) << 24) |
(static_cast<uint64_t>(d[5]) << 16) |
(static_cast<uint64_t>(d[6]) << 8) |
d[7];
}
};
现在我们将定义一组讨厌的宏,通过在结构 proto
中查找具有匹配名称的成员(每个 [=44 的私有结构)自动实现给定名称的访问器=]):
#define MEMBER_TYPE(MEMBER) typename std::decay<decltype(std::declval<proto>().MEMBER)>::type
#define IMPL_GETTER(MEMBER) MEMBER_TYPE(MEMBER) MEMBER() const { return ntoh_impl<MEMBER_TYPE(MEMBER)>::ntoh(data + offsetof(proto, MEMBER)); }
最后,我们有一个您给出的消息结构的示例实现:
class Message
{
private:
struct proto
{
uint32_t something1;
uint16_t something2;
};
public:
explicit Message(uint8_t const *p) : data(p) {}
explicit Message(char const *p) : data(reinterpret_cast<uint8_t const *>(p)) {}
IMPL_GETTER(something1)
IMPL_GETTER(something2)
private:
uint8_t const *data;
};
现在 Message::something1()
和 Message::something2()
已实现,并且将从 data
指针读取它们最终在 Message::proto
.
中相同的偏移量
提供 header 中的实现(有效内联)有可能在每个访问器的调用站点内联整个 ntoh
序列!
此 class 不拥有构建它的数据分配。如果这里有 ownership-maintaining 详细信息,您大概可以写一个基础 class。
我们正在将 C 语言的遗留代码重写为 C++。在我们系统的核心,我们有一个连接到 master 的 TCP 客户端。主人将不断地流式传输消息。每个套接字读取都会产生 N 条格式为 - {type, size, data[0]}
的消息。
现在我们不将这些消息复制到单独的缓冲区中 - 而是将指针传递给消息的开头、长度和 shared_ptr 到底层缓冲区给工人。
旧版 C 版本是单线程的,会像下面这样进行就地 NTOH 转换:
struct Message {
uint32_t something1;
uint16_t something2;
};
process (char *message)
Message *m = (message);
m->something1 = htonl(m->something1);
m->something2 = htons(m->something2);
然后使用消息
在登录新代码时遇到一些问题。
由于我们将消息分派给不同的工作人员,每个进行 ntoh 转换的工作人员都会导致缓存未命中问题,因为消息未在缓存中对齐 - 即没有填充 b/w留言。
同一条消息可以由不同的工作人员处理 - 这种情况下消息需要在本地处理并中继到另一个进程。这里中继工作者需要原始网络顺序的消息,本地工作需要转换为主机顺序。显然由于消息不重复,两者都不能满足。
我想到的解决方案是 -
复制该消息并发送一份给所有中继工作者(如果有的话)。在调度之前对属于同一缓冲区的所有消息在调度程序中执行 ntoh 转换 - 例如通过调用
handler->ntoh(message);
以便解决缓存未命中问题。向每位工人发送原件。每个worker将消息复制到本地缓冲区,然后进行ntoh转换并使用它。这里每个工作人员都可以使用线程特定的 (thread_local) 静态缓冲区作为暂存器来复制消息。
现在我的问题是
选项 1 是进行 ntoh 转换的方法 - C++sy 吗?我的意思是结构的对齐要求将不同于 char 缓冲区。 (我们还没有遇到任何问题。)。在这种情况下使用方案 2 应该没问题,因为临时缓冲区可以对齐 max_align_t,因此应该可以类型转换为任何结构。但这会导致复制整个消息 - 这可能会很大(比如几 K 大小)
有更好的处理方法吗?
您的主要问题似乎是如何处理未对齐的消息。也就是说,如果每个消息结构在其末尾没有足够的填充以使后续消息正确对齐,您可以通过将指向消息开头的指针重新解释为 object 来触发未对齐的读取。
我们可以通过多种方式解决这个问题,也许最简单的方法是 ntoh
基于 single-byte 指针,它实际上总是对齐的。
我们可以隐藏包装器 classes 背后令人讨厌的细节,它将采用指向消息开头的指针并具有将 ntoh
适当字段的访问器。
如评论中所述,要求偏移量由 C++ 结构确定,因为消息最初是这样创建的,可能不会打包。
首先,我们的 ntoh
实现是模板化的,因此我们可以 select 一个类型:
template <typename R>
struct ntoh_impl;
template <>
struct ntoh_impl<uint16_t>
{
static uint16_t ntoh(uint8_t const *d)
{
return (static_cast<uint16_t>(d[0]) << 8) |
d[1];
}
};
template <>
struct ntoh_impl<uint32_t>
{
static uint32_t ntoh(uint8_t const *d)
{
return (static_cast<uint32_t>(d[0]) << 24) |
(static_cast<uint32_t>(d[1]) << 16) |
(static_cast<uint32_t>(d[2]) << 8) |
d[3];
}
};
template<>
struct ntoh_impl<uint64_t>
{
static uint64_t ntoh(uint8_t const *d)
{
return (static_cast<uint64_t>(d[0]) << 56) |
(static_cast<uint64_t>(d[1]) << 48) |
(static_cast<uint64_t>(d[2]) << 40) |
(static_cast<uint64_t>(d[3]) << 32) |
(static_cast<uint64_t>(d[4]) << 24) |
(static_cast<uint64_t>(d[5]) << 16) |
(static_cast<uint64_t>(d[6]) << 8) |
d[7];
}
};
现在我们将定义一组讨厌的宏,通过在结构 proto
中查找具有匹配名称的成员(每个 [=44 的私有结构)自动实现给定名称的访问器=]):
#define MEMBER_TYPE(MEMBER) typename std::decay<decltype(std::declval<proto>().MEMBER)>::type
#define IMPL_GETTER(MEMBER) MEMBER_TYPE(MEMBER) MEMBER() const { return ntoh_impl<MEMBER_TYPE(MEMBER)>::ntoh(data + offsetof(proto, MEMBER)); }
最后,我们有一个您给出的消息结构的示例实现:
class Message
{
private:
struct proto
{
uint32_t something1;
uint16_t something2;
};
public:
explicit Message(uint8_t const *p) : data(p) {}
explicit Message(char const *p) : data(reinterpret_cast<uint8_t const *>(p)) {}
IMPL_GETTER(something1)
IMPL_GETTER(something2)
private:
uint8_t const *data;
};
现在 Message::something1()
和 Message::something2()
已实现,并且将从 data
指针读取它们最终在 Message::proto
.
提供 header 中的实现(有效内联)有可能在每个访问器的调用站点内联整个 ntoh
序列!
此 class 不拥有构建它的数据分配。如果这里有 ownership-maintaining 详细信息,您大概可以写一个基础 class。