将无符号字符数组转换为IP字符串的最快方法是什么

What is the fastest way to convert unsigned char array to IP string

本文关键字:方法 是什么 字符串 IP 字符 无符号 数组 转换      更新时间:2023-10-16

我需要(或多或少)实时处理其中的很多。我目前使用的方法是不再切割它。

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::vectorstd::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;
    }