符合标准的主机到网络字节顺序转换
Standard compliant host to network endianess conversion
我很惊讶 Whosebug 上有多少主题涉及找出系统的字节序和转换字节序。更令我惊讶的是,这两个问题竟然有数百种不同的答案。到目前为止,我所看到的所有提议的解决方案都是基于未定义的行为、非标准编译器扩展或 OS 特定的头文件。在我看来,如果现有答案给出了符合标准、高效(例如,使用 x86-bswap
)、编译时间启用的解决方案,则此问题只是一个重复问题。
肯定有一个符合标准的解决方案可用,我无法在一大堆旧的“hacky”解决方案中找到它。标准库不包含这样的功能也有些奇怪。也许对此类问题的态度正在改变,因为 C++20 在标准中引入了一种检测字节序的方法(通过 std::endian
),而 C++23 可能会包含 std::byteswap
,这会翻转字节序。
无论如何,我的问题是:
从哪个 C++ 标准开始,是否有可移植的符合标准的方式来执行主机到网络的字节顺序转换?
我在下面论证它在 C++20 中是可能的。我的代码是否正确,是否可以改进?
这种纯 C++ 解决方案是否应该优于 OS 特定函数,例如 POSIX-htonl
? (我想是的)
我想我可以给出一个 C++23 解决方案,它是 OS-独立的、高效的(没有系统调用,使用 x86-bswap
) 并可移植到小端和大端系统(但不能移植到混合端系统):
// requires C++23. see https://gcc.godbolt.org/z/6or1sEvKn
#include <type_traits>
#include <utility>
#include <bit>
constexpr inline auto host_to_net(std::integral auto i) {
static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
if constexpr (std::endian::native == std::endian::big) {
return i;
} else {
return std::byteswap(i);
}
}
由于 std::endian
在 C++20 中可用,因此可以通过实现 byteswap
手动。 中描述了一个解决方案,引用:
// requires C++17
#include <climits>
#include <cstdint>
#include <type_traits>
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) {
return ((((i >> (N * CHAR_BIT)) & (T)(unsigned char)(-1)) <<
((sizeof(T) - 1 - N) * CHAR_BIT)) | ...);
}; // ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) {
return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
链接的答案还提供了一个 C++11 byteswap
,但那个似乎效率较低 (not compiled to x86-bswap
)。我认为也应该有一种有效的 C++11 方法来做到这一点(使用更少的模板废话甚至更多),但我不关心旧的 C++,也没有真正尝试过。
假设我是正确的,剩下的问题是:能否在编译时以一种符合标准且与编译器无关的方式确定 C++20 之前的系统字节顺序? None 的答案 here 似乎确实实现了这一点。他们使用 reinterpret_cast
(不是编译时)、OS-header、联合别名(我相信这是 C++ 中的 UB)等。此外,出于某种原因,他们尝试“在 运行time" 尽管编译后的可执行文件将始终 运行 在相同的字节序下。)
可以在 constexpr 上下文之外执行此操作,并希望它被优化掉。另一方面,可以使用系统定义的预处理器定义并考虑所有平台,这似乎是 Boost 所采用的方法。或者也许(虽然我猜另一种方法更好?)使用宏并从网络库中选择平台特定的 htnl
风格的函数(完成,例如 here (GitHub))?
compile time-enabled solution.
首先考虑这是否是有用的要求。该程序不会在编译时与另一个系统通信。在什么情况下您需要在编译时常量上下文中使用序列化整数?
- Starting at what C++ standard is there a portable standard-compliant way of performing host to network byte order conversion?
从 C++98 开始,可以用标准 C++ 编写这样的函数。也就是说,后来的标准带来了美味的模板,使它变得更好。
最新标准的标准库中没有这个函数
- Should such a pure-c++ solution be preferred to OS specific functions such as, e.g., POSIX-htonl? (I think yes)
POSIX 的优点是编写测试以确保其正常工作不太重要。
纯 C++ 函数的优势在于,对于那些不符合 POSIX.
的平台,您不需要特定于平台的替代方案
此外,POSIX htonX 仅适用于 16 位和 32 位整数。您可以改用某些 *BSD 和 Linux (glibc) 中的 htobeXX 函数。
这是我自 C+17 以来一直在使用的。事前的一些注意事项:
由于字节序转换总是1 以进行序列化,因此我将结果直接写入缓冲区。转换为主机字节序时,我从缓冲区读取。
我不使用 CHAR_BIT
因为网络无论如何都不知道我的字节大小。网络字节是一个八位字节,如果您的 CPU 不同,那么这些功能将不起作用。正确处理 non-octet 字节是可能的,但不必要的工作,除非您需要在此类系统上支持网络通信。添加断言可能是个好主意。
我更喜欢称它为大端而不是“网络”端。 reader 有可能不知道 de-facto 网络字节序很大的约定。
与其检查“如果本机字节顺序是 X,则执行 Y,否则执行 Z”,我更愿意编写一个适用于所有本机字节顺序的函数。这可以通过位移来完成。
是的,它是 constexpr。不是因为它需要,而是因为它可以。我无法举出一个示例,其中删除 constexpr 会产生更糟糕的代码。
// helper to promote an integer type
template <class T>
using promote_t = std::decay_t<decltype(+std::declval<T>())>;
template <class T, std::size_t... I>
constexpr void
host_to_big_impl(
unsigned char* buf,
T t,
[[maybe_unused]] std::index_sequence<I...>) noexcept
{
using U = std::make_unsigned_t<promote_t<T>>;
constexpr U lastI = sizeof(T) - 1u;
constexpr U bits = 8u;
U u = t;
( (buf[I] = u >> ((lastI - I) * bits)), ... );
}
template <class T, std::size_t... I>
constexpr void
host_to_big(unsigned char* buf, T t) noexcept
{
using Indices = std::make_index_sequence<sizeof(T)>;
return host_to_big_impl<T>(buf, t, Indices{});
}
1 在我遇到的所有用例中。从整数到整数的转换可以通过委托来实现,如果你有这种情况,尽管由于需要 reinterpret_cast.
它们不能是 constexpr
我做了一个基准比较问题中的 C++ 解决方案和 eeroika 接受的答案中的解决方案。
看这个完全是浪费时间,但既然我做了,我想还是分享一下吧。结果是(在我查看的特定 not-quite-realistic 用例中)它们在性能方面似乎是等效的。尽管我的解决方案被编译为使用 x86-bswap
,而 eeroika 的解决方案仅使用 mov
.
当使用不同的编译器时,性能似乎有很大差异 (!!),我从这些基准测试中学到的主要内容是,我只是在浪费时间...
// benchmark to compare two C++20-stand-alone host-to-big-endian endianess conversion.]
// Run at quick-bench.com! This is not a complete program. (https://quick-bench.com/q/2qnr4xYKemKLZupsicVFV_09rEk)
// To run locally, include Google benchmark header and a main method as required by the benchmarking library.
// Adapted from
#include <type_traits>
#include <utility>
#include <cstddef>
#include <cstdint>
#include <climits>
#include <type_traits>
#include <utility>
#include <bit>
#include <random>
/////////////////////////////// Solution 1 ////////////////////////////////
template <typename T> struct scalar_t { T t{}; /* no begin/end */ };
static_assert(not std::ranges::range< scalar_t<int> >);
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) noexcept {
constexpr auto bits_per_byte = 8u;
static_assert(bits_per_byte == CHAR_BIT);
return ((((i >> (N * bits_per_byte)) & (T)(unsigned char)(-1)) <<
((sizeof(T) - 1 - N) * bits_per_byte)) | ...);
}; // ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) noexcept {
return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
constexpr inline auto host_to_net(std::integral auto i) {
static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
if constexpr (std::endian::native == std::endian::big) {
return i;
} else {
return bswap(i); // replace by `std::byteswap` once it's available!
}
}
/////////////////////////////// Solution 2 ////////////////////////////////
// helper to promote an integer type
template <class T>
using promote_t = std::decay_t<decltype(+std::declval<T>())>;
template <class T, std::size_t... I>
constexpr void
host_to_big_impl(
unsigned char* buf,
T t,
[[maybe_unused]] std::index_sequence<I...>) noexcept {
using U = std::make_unsigned_t<promote_t<T>>;
constexpr U lastI = sizeof(T) - 1u;
constexpr U bits = 8u;
U u = t;
( (buf[I] = u >> ((lastI - I) * bits)), ... );
}
template <class T, std::size_t... I>
constexpr void
host_to_big(unsigned char* buf, T t) noexcept {
using Indices = std::make_index_sequence<sizeof(T)>;
return host_to_big_impl<T>(buf, t, Indices{});
}
//////////////////////// Benchmarks ////////////////////////////////////
template<std::integral T>
std::vector<T> get_random_vector(std::size_t length, unsigned int seed) {
// NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. Don't use in production code!
std::mt19937_64 rng{seed};
std::uniform_int_distribution<T> distribution(
std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
std::vector<T> result(length);
for (auto && val : result) {
val = distribution(rng);
}
return result;
}
template<>
std::vector<bool> get_random_vector<bool>(std::size_t length, unsigned int seed) {
// NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. ONLY USE FOR TESTING!
std::mt19937_64 rng{seed};
std::bernoulli_distribution distribution{0.5};
std::vector<bool> vec(length);
for (auto && val : vec) {
val = distribution(rng);
}
return vec;
}
constexpr std::size_t n_ints{1000};
static void solution1(benchmark::State& state) {
std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());
for (auto _ : state) {
for (std::size_t i{}; i < intvec.size(); ++i) {
host_to_big(buffer.data() + sizeof(int)*i, intvec[i]);
}
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
BENCHMARK(solution1);
static void solution2(benchmark::State& state) {
std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());
for (auto _ : state) {
for (std::size_t i{}; i < intvec.size(); ++i) {
buffer[sizeof(int)*i] = host_to_net(intvec[i]);
}
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
BENCHMARK(solution2);
我很惊讶 Whosebug 上有多少主题涉及找出系统的字节序和转换字节序。更令我惊讶的是,这两个问题竟然有数百种不同的答案。到目前为止,我所看到的所有提议的解决方案都是基于未定义的行为、非标准编译器扩展或 OS 特定的头文件。在我看来,如果现有答案给出了符合标准、高效(例如,使用 x86-bswap
)、编译时间启用的解决方案,则此问题只是一个重复问题。
肯定有一个符合标准的解决方案可用,我无法在一大堆旧的“hacky”解决方案中找到它。标准库不包含这样的功能也有些奇怪。也许对此类问题的态度正在改变,因为 C++20 在标准中引入了一种检测字节序的方法(通过 std::endian
),而 C++23 可能会包含 std::byteswap
,这会翻转字节序。
无论如何,我的问题是:
从哪个 C++ 标准开始,是否有可移植的符合标准的方式来执行主机到网络的字节顺序转换?
我在下面论证它在 C++20 中是可能的。我的代码是否正确,是否可以改进?
这种纯 C++ 解决方案是否应该优于 OS 特定函数,例如 POSIX-
htonl
? (我想是的)
我想我可以给出一个 C++23 解决方案,它是 OS-独立的、高效的(没有系统调用,使用 x86-bswap
) 并可移植到小端和大端系统(但不能移植到混合端系统):
// requires C++23. see https://gcc.godbolt.org/z/6or1sEvKn
#include <type_traits>
#include <utility>
#include <bit>
constexpr inline auto host_to_net(std::integral auto i) {
static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
if constexpr (std::endian::native == std::endian::big) {
return i;
} else {
return std::byteswap(i);
}
}
由于 std::endian
在 C++20 中可用,因此可以通过实现 byteswap
手动。
// requires C++17
#include <climits>
#include <cstdint>
#include <type_traits>
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) {
return ((((i >> (N * CHAR_BIT)) & (T)(unsigned char)(-1)) <<
((sizeof(T) - 1 - N) * CHAR_BIT)) | ...);
}; // ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) {
return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
链接的答案还提供了一个 C++11 byteswap
,但那个似乎效率较低 (not compiled to x86-bswap
)。我认为也应该有一种有效的 C++11 方法来做到这一点(使用更少的模板废话甚至更多),但我不关心旧的 C++,也没有真正尝试过。
假设我是正确的,剩下的问题是:能否在编译时以一种符合标准且与编译器无关的方式确定 C++20 之前的系统字节顺序? None 的答案 here 似乎确实实现了这一点。他们使用 reinterpret_cast
(不是编译时)、OS-header、联合别名(我相信这是 C++ 中的 UB)等。此外,出于某种原因,他们尝试“在 运行time" 尽管编译后的可执行文件将始终 运行 在相同的字节序下。)
可以在 constexpr 上下文之外执行此操作,并希望它被优化掉。另一方面,可以使用系统定义的预处理器定义并考虑所有平台,这似乎是 Boost 所采用的方法。或者也许(虽然我猜另一种方法更好?)使用宏并从网络库中选择平台特定的 htnl
风格的函数(完成,例如 here (GitHub))?
compile time-enabled solution.
首先考虑这是否是有用的要求。该程序不会在编译时与另一个系统通信。在什么情况下您需要在编译时常量上下文中使用序列化整数?
- Starting at what C++ standard is there a portable standard-compliant way of performing host to network byte order conversion?
从 C++98 开始,可以用标准 C++ 编写这样的函数。也就是说,后来的标准带来了美味的模板,使它变得更好。
最新标准的标准库中没有这个函数
- Should such a pure-c++ solution be preferred to OS specific functions such as, e.g., POSIX-htonl? (I think yes)
POSIX 的优点是编写测试以确保其正常工作不太重要。
纯 C++ 函数的优势在于,对于那些不符合 POSIX.
的平台,您不需要特定于平台的替代方案此外,POSIX htonX 仅适用于 16 位和 32 位整数。您可以改用某些 *BSD 和 Linux (glibc) 中的 htobeXX 函数。
这是我自 C+17 以来一直在使用的。事前的一些注意事项:
由于字节序转换总是1 以进行序列化,因此我将结果直接写入缓冲区。转换为主机字节序时,我从缓冲区读取。
我不使用
CHAR_BIT
因为网络无论如何都不知道我的字节大小。网络字节是一个八位字节,如果您的 CPU 不同,那么这些功能将不起作用。正确处理 non-octet 字节是可能的,但不必要的工作,除非您需要在此类系统上支持网络通信。添加断言可能是个好主意。我更喜欢称它为大端而不是“网络”端。 reader 有可能不知道 de-facto 网络字节序很大的约定。
与其检查“如果本机字节顺序是 X,则执行 Y,否则执行 Z”,我更愿意编写一个适用于所有本机字节顺序的函数。这可以通过位移来完成。
是的,它是 constexpr。不是因为它需要,而是因为它可以。我无法举出一个示例,其中删除 constexpr 会产生更糟糕的代码。
// helper to promote an integer type template <class T> using promote_t = std::decay_t<decltype(+std::declval<T>())>; template <class T, std::size_t... I> constexpr void host_to_big_impl( unsigned char* buf, T t, [[maybe_unused]] std::index_sequence<I...>) noexcept { using U = std::make_unsigned_t<promote_t<T>>; constexpr U lastI = sizeof(T) - 1u; constexpr U bits = 8u; U u = t; ( (buf[I] = u >> ((lastI - I) * bits)), ... ); } template <class T, std::size_t... I> constexpr void host_to_big(unsigned char* buf, T t) noexcept { using Indices = std::make_index_sequence<sizeof(T)>; return host_to_big_impl<T>(buf, t, Indices{}); }
1 在我遇到的所有用例中。从整数到整数的转换可以通过委托来实现,如果你有这种情况,尽管由于需要 reinterpret_cast.
它们不能是 constexpr我做了一个基准比较问题中的 C++ 解决方案和 eeroika 接受的答案中的解决方案。
看这个完全是浪费时间,但既然我做了,我想还是分享一下吧。结果是(在我查看的特定 not-quite-realistic 用例中)它们在性能方面似乎是等效的。尽管我的解决方案被编译为使用 x86-bswap
,而 eeroika 的解决方案仅使用 mov
.
当使用不同的编译器时,性能似乎有很大差异 (!!),我从这些基准测试中学到的主要内容是,我只是在浪费时间...
// benchmark to compare two C++20-stand-alone host-to-big-endian endianess conversion.]
// Run at quick-bench.com! This is not a complete program. (https://quick-bench.com/q/2qnr4xYKemKLZupsicVFV_09rEk)
// To run locally, include Google benchmark header and a main method as required by the benchmarking library.
// Adapted from
#include <type_traits>
#include <utility>
#include <cstddef>
#include <cstdint>
#include <climits>
#include <type_traits>
#include <utility>
#include <bit>
#include <random>
/////////////////////////////// Solution 1 ////////////////////////////////
template <typename T> struct scalar_t { T t{}; /* no begin/end */ };
static_assert(not std::ranges::range< scalar_t<int> >);
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) noexcept {
constexpr auto bits_per_byte = 8u;
static_assert(bits_per_byte == CHAR_BIT);
return ((((i >> (N * bits_per_byte)) & (T)(unsigned char)(-1)) <<
((sizeof(T) - 1 - N) * bits_per_byte)) | ...);
}; // ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) noexcept {
return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
constexpr inline auto host_to_net(std::integral auto i) {
static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
if constexpr (std::endian::native == std::endian::big) {
return i;
} else {
return bswap(i); // replace by `std::byteswap` once it's available!
}
}
/////////////////////////////// Solution 2 ////////////////////////////////
// helper to promote an integer type
template <class T>
using promote_t = std::decay_t<decltype(+std::declval<T>())>;
template <class T, std::size_t... I>
constexpr void
host_to_big_impl(
unsigned char* buf,
T t,
[[maybe_unused]] std::index_sequence<I...>) noexcept {
using U = std::make_unsigned_t<promote_t<T>>;
constexpr U lastI = sizeof(T) - 1u;
constexpr U bits = 8u;
U u = t;
( (buf[I] = u >> ((lastI - I) * bits)), ... );
}
template <class T, std::size_t... I>
constexpr void
host_to_big(unsigned char* buf, T t) noexcept {
using Indices = std::make_index_sequence<sizeof(T)>;
return host_to_big_impl<T>(buf, t, Indices{});
}
//////////////////////// Benchmarks ////////////////////////////////////
template<std::integral T>
std::vector<T> get_random_vector(std::size_t length, unsigned int seed) {
// NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. Don't use in production code!
std::mt19937_64 rng{seed};
std::uniform_int_distribution<T> distribution(
std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
std::vector<T> result(length);
for (auto && val : result) {
val = distribution(rng);
}
return result;
}
template<>
std::vector<bool> get_random_vector<bool>(std::size_t length, unsigned int seed) {
// NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. ONLY USE FOR TESTING!
std::mt19937_64 rng{seed};
std::bernoulli_distribution distribution{0.5};
std::vector<bool> vec(length);
for (auto && val : vec) {
val = distribution(rng);
}
return vec;
}
constexpr std::size_t n_ints{1000};
static void solution1(benchmark::State& state) {
std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());
for (auto _ : state) {
for (std::size_t i{}; i < intvec.size(); ++i) {
host_to_big(buffer.data() + sizeof(int)*i, intvec[i]);
}
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
BENCHMARK(solution1);
static void solution2(benchmark::State& state) {
std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());
for (auto _ : state) {
for (std::size_t i{}; i < intvec.size(); ++i) {
buffer[sizeof(int)*i] = host_to_net(intvec[i]);
}
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
BENCHMARK(solution2);