簡體   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