繁体   English   中英

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

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

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

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对象的成本很高。 它将受控序列存储在 freestore 内存(通常实现为堆内存)中,这在 C++(和 C)中分配的成本有些高。
  • 使用std::vector存储 4 字节值会不必要地消耗资源。 它也在 freestore 中分配内存。 将其替换为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] = '\0';

// 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] = '\0';
        LOOKUP_TABLE[i][2] = '\0';
        LOOKUP_TABLE[i][3] = '\0';
    }

    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] = '\0';
        LOOKUP_TABLE[i][3] = '\0';
    }
    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] = '\0';
    }
}

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], '\0');
    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], '\0');
    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;
}

UPD 2我想我找到了一种避免额外副本的方法(尽管返回时仍然有额外的副本)。 这是带有查找表和不带它的版本

#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], '\0');
    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我做了一些测量(1 000 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 说明。

顺便说一句,通常您的代码越原始,速度就越快。 我会使用无符号字符数组而不是向量,字符数组而不是字符串,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;
    }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM