我需要(或多或少)实时处理其中的很多。我目前使用的方法是不再切割它。
std::string parse_ipv4_address( const std::vector<unsigned char> & data, int start )
{
char ip_addr[16];
snprintf( ip_addr, sizeof(ip_addr), "%d.%d.%d.%d",
data[start + 0], data[start + 1], data[start + 2], data[start + 3] );
return std::string( ip_addr );
}
// used like this
std::vector<unsigned char> ip = { 0xc0, 0xa8, 0x20, 0x0c };
std::string ip_address = parse_ipv4_address( ip, 0 );
std::cout << ip_address << std::endl; // not actually printed in real code
// produces 192.168.32.12
有更快的方法吗?如何?
注意!性能是这里的问题,所以这个问题不是重复的。
以下是影响性能的潜在候选者:
snprintf
需要解析格式字符串,并执行错误处理。任何一种都需要花费时间来实现您不需要的功能- 在返回时构造
std::string
对象的成本很高。它将受控序列存储在自由存储内存(通常实现为堆内存)中,这在C++(和C)中的分配成本有些高 - 使用
std::vector
来存储4字节的值不必要地消耗资源。它也在自由存储中分配内存。将其替换为char[4]
或32位整数(uint32_t
)
由于您不需要printf
函数族的多功能性,因此可以完全放弃它,转而使用查找表。查找表由256个条目组成,每个条目保存字节值0到255的可视表示。为了优化这一点,让LUT也包含一个尾随的.
字符。(需要特别小心,以解决端序问题。我假设这里是小端序。)
一个解决方案可能看起来像这样1):
const uint32_t mapping[] = { 0x2E303030, // "000."
0x2E313030, // "001."
// ...
0x2E343532, // "254."
0x2E353532 // "255."
};
alignas(uint32_t) char ip_addr[16];
uint32_t* p = reinterpret_cast<uint32_t*>(&ip_addr[0]);
p[0] = mapping[data[0]];
p[1] = mapping[data[1]];
p[2] = mapping[data[2]];
p[3] = mapping[data[3]];
// Zero-terminate string (overwriting the superfluous trailing .-character)
ip_addr[15] = ' ';
// As a final step, waste all the hard earned savings by constructing a std::string.
// (as an ironic twist, let's pick the c'tor with the best performance)
return std::string(&ip_addr[0], &ip_addr[15]);
// A more serious approach would either return the array (ip_addr), or have the caller
// pass in a pre-allocated array for output.
return ip_addr;
1)免责声明:从
char*
到uint32_t*
的铸造在技术上表现出未定义的行为不要使用,除非您的平台(编译器和操作系统)提供了额外的保证,使其定义良好
三个
一个价格的四个答案。
首先,确保你在优化正确的部分。std::vector
和std::string
的创建都涉及内存分配,而cout <<
可能涉及文件访问、图形等!
第二:不要用矢量来表示一个IP地址的4字节。只需使用char ip[4]
,甚至32位整数
第三:我猜你处理的不是完全随机的IP地址。可能有几十万个不同的地址?在这种情况下,使用std::map<INT32, std::string>
作为缓存,并根据需要从缓存中提取所需的,根据需要写入新的。如果缓存太大,就清空它,然后重新开始。。。
第四:考虑用十六进制点四进制表示法写入IP地址。这仍然被
inet_addr()
等调用所接受,并且有几个优点:所有字段都是固定宽度的,只有8个"字符"需要更新,二进制到十六进制的转换通常比二进制到十进制更快。https://en.wikipedia.org/wiki/IPv4#Address_representations
查找表可以在这里使用(在程序启动时初始化)。我想你已经配置了评测,所以我没有评测解决方案,不知道会有什么结果,所以请在你得到一些结果时分享。
char LOOKUP_TABLE[256][4];
void init_lookup_table() {
char digits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
for (int i = 0; i < 10; ++i) {
LOOKUP_TABLE[i][0] = digits[i % 10];
LOOKUP_TABLE[i][1] = ' ';
LOOKUP_TABLE[i][2] = ' ';
LOOKUP_TABLE[i][3] = ' ';
}
for (int i = 10; i < 100; ++i) {
LOOKUP_TABLE[i][0] = digits[(i / 10) % 10];
LOOKUP_TABLE[i][1] = digits[i % 10];
LOOKUP_TABLE[i][2] = ' ';
LOOKUP_TABLE[i][3] = ' ';
}
for (int i = 100; i < 256; ++i) {
LOOKUP_TABLE[i][0] = digits[(i / 100) % 10];
LOOKUP_TABLE[i][1] = digits[(i / 10) % 10];
LOOKUP_TABLE[i][2] = digits[i % 10];
LOOKUP_TABLE[i][3] = ' ';
}
}
void append_octet(char **buf, unsigned char value, char terminator) {
char *src = LOOKUP_TABLE[value];
if (value < 10) {
(*buf)[0] = src[0];
(*buf)[1] = terminator;
(*buf) += 2;
}
else if (value < 100) {
(*buf)[0] = src[0];
(*buf)[1] = src[1];
(*buf)[2] = terminator;
(*buf) += 3;
}
else {
(*buf)[0] = src[0];
(*buf)[1] = src[1];
(*buf)[2] = src[2];
(*buf)[3] = terminator;
(*buf) += 4;
}
}
std::string parse_ipv4_address( const std::vector<unsigned char> & data, int start ) {
char ip_addr[16];
char *dst = ip_addr;
append_octet(&dst, data[start + 0], '.');
append_octet(&dst, data[start + 1], '.');
append_octet(&dst, data[start + 2], '.');
append_octet(&dst, data[start + 3], ' ');
return std::string( ip_addr );
}
int main() {
init_lookup_table();
std::vector<unsigned char> ip = { 0xc0, 0x8, 0x20, 0x0c };
std::cout << parse_ipv4_address( ip, 0 ) << std::endl;
}
提高性能的另一种方法是用专门的对象替换字符串。在这种情况下,您将能够实现所需的I/O方法(我的猜测是,您需要字符串将其打印到某个地方),并且将免于在字符串构造上进行复制。
UPD仔细想想,我想我的代码查找表已经过时了,所以可以直接将用于构建查找表的代码复制到append_octet
,使digits
全局化。
更新的代码(感谢MikeMB和Matteo Italia),看起来也非常适合缓存
inline void append_octet(char **buf, unsigned char value, char terminator) {
if (value < 10) {
(*buf)[0] = '0' + (value % 10);
(*buf)[1] = terminator;
(*buf) += 2;
}
else if (value < 100) {
(*buf)[0] = '0' + ((value / 10) % 10);
(*buf)[1] = '0' + (value % 10);
(*buf)[2] = terminator;
(*buf) += 3;
}
else {
(*buf)[0] = '0' + ((value / 100) % 10);
(*buf)[1] = '0' + ((value / 10) % 10);
(*buf)[2] = '0' + (value % 10);
(*buf)[3] = terminator;
(*buf) += 4;
}
}
std::string parse_ipv4_address( const std::vector<unsigned char> & data, int start ) {
char ip_addr[16];
char *dst = ip_addr;
append_octet(&dst, data[start + 0], '.');
append_octet(&dst, data[start + 1], '.');
append_octet(&dst, data[start + 2], '.');
append_octet(&dst, data[start + 3], ' ');
return std::string( ip_addr );
}
int main() {
std::vector<unsigned char> ip = { 0xc0, 0x8, 0x20, 0x0c };
std::cout << parse_ipv4_address( ip, 0 ) << std::endl;
}
UPD2我想我找到了一种避免额外副本的方法(尽管返回时仍有额外副本)。这是带有查找表和不带的版本
#include <string>
#include <iostream>
#include <vector>
std::string LUT[256];
void init_lookup_table() {
for (int i = 0; i < 10; ++i) {
LUT[i].reserve(2);
LUT[i].push_back('0' + i);
LUT[i].push_back('.');
}
for (int i = 10; i < 100; ++i) {
LUT[i].reserve(3);
LUT[i].push_back('0' + (i/10));
LUT[i].push_back('0' + (i%10));
LUT[i].push_back('.');
}
for (int i = 100; i < 256; ++i) {
LUT[i].reserve(4);
LUT[i].push_back('0' + (i/100));
LUT[i].push_back('0' + ((i/10)%10));
LUT[i].push_back('0' + (i%10));
LUT[i].push_back('.');
}
}
std::string parse_ipv4_address_lut( const std::vector<unsigned char> & data, int start ) {
std::string res;
res.reserve(16);
res.append(LUT[data[start + 0]]);
res.append(LUT[data[start + 1]]);
res.append(LUT[data[start + 2]]);
res.append(LUT[data[start + 3]]);
res.pop_back();
return res;
}
inline void append_octet_calc(std::string *str, unsigned char value, char terminator) {
if (value < 10) {
str->push_back('0' + (value % 10));
str->push_back(terminator);
}
else if (value < 100) {
str->push_back('0' + ((value / 10) % 10));
str->push_back('0' + (value % 10));
str->push_back(terminator);
}
else {
str->push_back('0' + ((value / 100) % 10));
str->push_back('0' + ((value / 10) % 10));
str->push_back('0' + (value % 10));
str->push_back(terminator);
}
}
std::string parse_ipv4_address_calc( const std::vector<unsigned char> & data, int start ) {
std::string res;
res.reserve(16);
append_octet_calc(&res, data[start + 0], '.');
append_octet_calc(&res, data[start + 1], '.');
append_octet_calc(&res, data[start + 2], '.');
append_octet_calc(&res, data[start + 3], ' ');
return res;
}
int main() {
init_lookup_table();
std::vector<unsigned char> ip = { 0xc0, 0x8, 0x20, 0x0c };
std::cout << parse_ipv4_address_calc( ip, 0 ) << std::endl;
std::cout << parse_ipv4_address_lut( ip, 0 ) << std::endl;
}
UPD 3我进行了一些测量(1000 000次重复)
clang++ -O3
orig...done in 5053 ms // original implementation by OP
c_lut...done in 2083 ms // lookup table -> char[] -> std::string
c_calc...done in 2245 ms // calculate -> char[] -> std::string
cpp_lut...done in 2597 ms // lookup table + std::string::reserve + append
cpp_calc...done in 2632 ms // calculate -> std::string::reserve + push_back
hardcore...done in 1512 ms // reinterpret_cast solution by @IInspectable
g++ -O3
orig...done in 5598 ms // original implementation by OP
c_lut...done in 2285 ms // lookup table -> char[] -> std::string
c_calc...done in 2307 ms // calculate -> char[] -> std::string
cpp_lut...done in 2622 ms // lookup table + std::string::reserve + append
cpp_calc...done in 2601 ms // calculate -> std::string::reserve + push_back
hardcore...done in 1576 ms // reinterpret_cast solution by @IInspectable
请注意,"硬核"解决方案由于前导零而不等价。
您可以使用一个查找表,其中包含0到255之间的数字字符串。如果速度非常重要,您也可以使用inline关键字或宏来实现函数。您也可以查看sse说明。
顺便说一句,通常你的代码越原始,速度就越快。我会用无符号的char数组代替矢量,用char数组代替字符串,用memcpy(甚至是直接逐字节复制)代替sprintf。
给你。。。
std::string IP_parse(unsigned char data[4])
{
std::string parsedString = "";
snprintf((char*)parsedString.c_str(), sizeof(char[15]), "%d.%d.%d.%d", data[0], data[1], data[2], data[3]);
return parsedString;
}