C++17 std::optional 深拷贝 vs 引用:unordered_map 查询大对象性能对比

如何在使用 optional 返回 unordered_map 查询结果的时候不触发数据拷贝

先看下最初的代码实现:

C++ 复制代码
#include <iostream>
#include <unordered_map>
#include <optional>
#include <cstdint>
#include <string>

// -------------------- Key 结构体 --------------------
struct Key {
    uint16_t part1;
    uint16_t part2;

    bool operator==(const Key &other) const noexcept {
        return part1 == other.part1 && part2 == other.part2;
    }
};

// 为 Key 提供哈希函数
struct KeyHash {
    std::size_t operator()(const Key &k) const noexcept {
        return (static_cast<std::size_t>(k.part1) << 16) ^ k.part2;
    }
};

// -------------------- Value 结构体 --------------------
enum class Status: uint8_t {
    OK,
    Warning,
    Error
};

struct Detail {
    int count;
    std::string description;
};

struct Value {
    bool flag;
    Status status;
    Detail detail;

    // 日志辅助:追踪拷贝/移动
    Value(bool f, Status s, Detail d) : flag(f), status(s), detail(std::move(d)) {
        std::cout << "Value constructed\n";
    }

    Value(const Value &other) : flag(other.flag), status(other.status), detail(other.detail) {
        std::cout << "Value copied\n";
    }

    Value(Value &&other) noexcept
        : flag(other.flag), status(other.status), detail(std::move(other.detail)) {
        std::cout << "Value moved\n";
    }
};

// -------------------- 全局 unordered_map,带初始化 --------------------
std::unordered_map<Key, Value, KeyHash> globalMap = {
    {{1, 100}, {true, Status::OK, {42, "All good"}}},
    {{2, 200}, {false, Status::Warning, {7, "Check input"}}},
    {{3, 300}, {true, Status::Error, {0, "Failure"}}}
};

// -------------------- 查找函数 --------------------
std::optional<Value> findValue(const Key &key) {
    auto it = globalMap.find(key);
    if (it != globalMap.end()) {
        return it->second; // 会发生一次拷贝
    }
    return std::nullopt;
}

// -------------------- 示例使用 --------------------
int main() {
    std::cout << "--- 查找存在的 key ---\n";
    if (auto result = findValue({2, 200})) {
        std::cout << "Found! flag=" << result->flag
                << " description=" << result->detail.description << "\n";
    } else {
        std::cout << "Not found!\n";
    }

    std::cout << "--- 查找不存在的 key ---\n";
    if (auto result = findValue({9, 900})) {
        std::cout << "Found!\n";
    } else {
        std::cout << "Not found!\n";
    }

    return 0;
}

findValue 的功能是在 globalMap 这个 std::unordered_map<Key, Value, KeyHash> 中根据 Key 查找对应的 Value,无论找到与否都包装到 optional 中返回。其中 KeyValue 都是结构体,且 Value 尤其复杂,还嵌套了其他结构体。

上面代码编译运行后控制台输出如下:

lua 复制代码
Value constructed
Value copied
Value constructed
Value copied
Value constructed
Value copied
Value copied
Value copied
Value copied
--- 查找存在的 key ---
Value copied
Found! flag=0 description=Check input
--- 查找不存在的 key ---
Not found!

从"--- 查找存在的 key ---"开始是 main 函数的执行输出。可以看到在"查找存在的 key"这个场景下 findValue 在返回结果时发生了一次拷贝。

解决办法是使用 std::reference_wrapper,将 findValue 函数从返回 std::optional<Value> 改成返回 std::optional<std::reference_wrapper<const Value> >。但要注意一点:返回的引用有效期取决于 globalMap,调用者不能存储这个引用超过 globalMap 生命周期。

修改后的代码如下:

C++ 复制代码
#include <iostream>
#include <unordered_map>
#include <optional>
#include <cstdint>
#include <string>

// -------------------- Key 结构体 --------------------
struct Key {
    uint16_t part1;
    uint16_t part2;

    bool operator==(const Key &other) const noexcept {
        return part1 == other.part1 && part2 == other.part2;
    }
};

// 为 Key 提供哈希函数
struct KeyHash {
    std::size_t operator()(const Key &k) const noexcept {
        return (static_cast<std::size_t>(k.part1) << 16) ^ k.part2;
    }
};

// -------------------- Value 结构体 --------------------
enum class Status: uint8_t {
    OK,
    Warning,
    Error
};

struct Detail {
    int count;
    std::string description;
};

struct Value {
    bool flag;
    Status status;
    Detail detail;

    // 日志辅助:追踪拷贝/移动
    Value(bool f, Status s, Detail d) : flag(f), status(s), detail(std::move(d)) {
        std::cout << "Value constructed\n";
    }

    Value(const Value &other) : flag(other.flag), status(other.status), detail(other.detail) {
        std::cout << "Value copied\n";
    }

    Value(Value &&other) noexcept
        : flag(other.flag), status(other.status), detail(std::move(other.detail)) {
        std::cout << "Value moved\n";
    }
};

// -------------------- 全局 unordered_map,带初始化 --------------------
std::unordered_map<Key, Value, KeyHash> globalMap = {
    {{1, 100}, {true, Status::OK, {42, "All good"}}},
    {{2, 200}, {false, Status::Warning, {7, "Check input"}}},
    {{3, 300}, {true, Status::Error, {0, "Failure"}}}
};

// -------------------- 查找函数(避免复制) --------------------
std::optional<std::reference_wrapper<const Value> > findValue(const Key &key) {
    auto it = globalMap.find(key);
    if (it != globalMap.end()) {
        return std::cref(it->second); // 返回引用包装器
    }
    return std::nullopt;
}

// -------------------- 示例使用 --------------------
int main() {
    std::cout << "--- 查找存在的 key ---\n";
    if (auto result = findValue({2, 200})) {
        // 注意 result 是 optional<reference_wrapper<const Value>>
        const Value &v = result->get();
        std::cout << "Found! flag=" << v.flag
                << " description=" << v.detail.description << "\n";
    } else {
        std::cout << "Not found!\n";
    }

    std::cout << "--- 查找不存在的 key ---\n";
    if (auto result = findValue({9, 900})) {
        std::cout << "Found!\n";
    } else {
        std::cout << "Not found!\n";
    }

    return 0;
}

编译运行后控制台输出如下:

lua 复制代码
Value constructed
Value copied
Value constructed
Value copied
Value constructed
Value copied
Value copied
Value copied
Value copied
--- 查找存在的 key ---
Found! flag=0 description=Check input
--- 查找不存在的 key ---
Not found!

可以看到,这样实现后,findValue 函数执行过程中不会发生任何对 Value 的数据复制或移动。

性能对比:使用与不使用 reference_wrapper

下面对比修改前后的性能,分成查找存在的 key 和查找不存在的 key 两种测试场景。globalMap 中键值对的数量为 1024,两种测试场景均分别调用修改前后的 findValue 函数查询 10240000 次。为了让性能差异更显著,在 Detail 结构体中新增了 std::vector<double> numbers 字段,从而让 Value 变得更大。测试过程中的 checksum 计算是为了确保查询动作不会被编译器优化掉。

C++ 复制代码
#include <iostream>
#include <unordered_map>
#include <optional>
#include <cstdint>
#include <string>
#include <functional>
#include <chrono>
#include <vector>
#include <random>
#include <cassert>

// -------------------- Key 结构体 --------------------
struct Key {
    uint16_t part1;
    uint16_t part2;

    bool operator==(const Key &other) const noexcept {
        return part1 == other.part1 && part2 == other.part2;
    }
};

// 为 Key 提供哈希函数
struct KeyHash {
    std::size_t operator()(const Key &k) const noexcept {
        return (static_cast<std::size_t>(k.part1) << 16) ^ k.part2;
    }
};

// -------------------- Value 结构体 --------------------
enum class Status: uint8_t {
    OK,
    Warning,
    Error
};

struct Detail {
    int count;
    std::string description;
    std::vector<double> numbers; // 新增字段,为了演示在Value结构体比较大时的性能差异
};

struct Value {
    bool flag;
    Status status;
    Detail detail;
};

// -------------------- 全局 map --------------------
std::unordered_map<Key, Value, KeyHash> globalMap;

// -------------------- 版本 A:返回 optional<Value> --------------------
std::optional<Value> findValueA(const Key &key) {
    auto it = globalMap.find(key);
    if (it != globalMap.end()) {
        return it->second; // 发生复制
    }
    return std::nullopt;
}

// -------------------- 版本 B:返回 optional<reference_wrapper<const Value>> --------------------
std::optional<std::reference_wrapper<const Value> > findValueB(const Key &key) {
    auto it = globalMap.find(key);
    if (it != globalMap.end()) {
        return std::cref(it->second); // 避免复制
    }
    return std::nullopt;
}

// -------------------- 性能测试 --------------------
int main() {
    constexpr int EXIST_N = 1024; // 存在 key 数量
    constexpr int NONEXIST_N = 1024; // 不存在 key 数量
    constexpr int REPEAT = 10000; // 每个集合重复查找次数(放大测试量)

    std::mt19937 rng(42);
    std::uniform_real_distribution<double> distReal(0.0, 1.0);

    std::vector<Key> existKeys;
    std::vector<Key> nonexistKeys;

    // 生成存在的 key 集合
    for (int i = 0; i < EXIST_N; ++i) {
        existKeys.push_back({static_cast<uint16_t>((i >> 5) & 0xFFFF), static_cast<uint16_t>(i & 0xFFFF)});
    }

    // 插入到 globalMap(每个 Detail 带有 200 个随机 double)
    for (int i = 0; i < EXIST_N; ++i) {
        Detail d{i, "data_" + std::to_string(i)};
        for (int j = 0; j < 200; ++j) {
            d.numbers.push_back(distReal(rng));
        }
        globalMap[existKeys[i]] = Value{(i % 2 == 0), Status::OK, std::move(d)};
    }

    // 生成不存在的 key 集合(与存在 key 偏移,确保无交集)
    for (int i = 0; i < NONEXIST_N; ++i) {
        nonexistKeys.push_back({
            static_cast<uint16_t>(((i + EXIST_N + 1000) >> 5) & 0xFFFF),
            static_cast<uint16_t>((i + EXIST_N + 1000) & 0xFFFF)
        });
    }

    // 确保两个 key 集合没有交集
    for (auto &k: nonexistKeys) {
        assert(globalMap.find(k) == globalMap.end());
    }

    // -------------------- 测试存在 key (A) --------------------
    auto startA_exist = std::chrono::high_resolution_clock::now();
    double checksumA_exist = 0;
    for (int r = 0; r < REPEAT; ++r) {
        for (auto &k: existKeys) {
            auto res = findValueA(k);
            if (res) {
                checksumA_exist += res->detail.numbers[0];
            }
        }
    }
    auto endA_exist = std::chrono::high_resolution_clock::now();
    auto durA_exist = std::chrono::duration_cast<std::chrono::milliseconds>(endA_exist - startA_exist).count();

    // -------------------- 测试存在 key (B) --------------------
    auto startB_exist = std::chrono::high_resolution_clock::now();
    double checksumB_exist = 0;
    for (int r = 0; r < REPEAT; ++r) {
        for (auto &k: existKeys) {
            auto res = findValueB(k);
            if (res) {
                checksumB_exist += res->get().detail.numbers[0];
            }
        }
    }
    auto endB_exist = std::chrono::high_resolution_clock::now();
    auto durB_exist = std::chrono::duration_cast<std::chrono::milliseconds>(endB_exist - startB_exist).count();

    // -------------------- 测试不存在 key (A) --------------------
    auto startA_non = std::chrono::high_resolution_clock::now();
    double checksumA_non = 0;
    for (int r = 0; r < REPEAT; ++r) {
        for (auto &k: nonexistKeys) {
            auto res = findValueA(k);
            if (res) {
                checksumA_non += res->detail.numbers[0];
            }
        }
    }
    auto endA_non = std::chrono::high_resolution_clock::now();
    auto durA_non = std::chrono::duration_cast<std::chrono::milliseconds>(endA_non - startA_non).count();

    // -------------------- 测试不存在 key (B) --------------------
    auto startB_non = std::chrono::high_resolution_clock::now();
    double checksumB_non = 0;
    for (int r = 0; r < REPEAT; ++r) {
        for (auto &k: nonexistKeys) {
            auto res = findValueB(k);
            if (res) {
                checksumB_non += res->get().detail.numbers[0];
            }
        }
    }
    auto endB_non = std::chrono::high_resolution_clock::now();
    auto durB_non = std::chrono::duration_cast<std::chrono::milliseconds>(endB_non - startB_non).count();

    // -------------------- 输出结果 --------------------
    std::cout << "==== 存在 key 测试 ====\n";
    std::cout << "Version A (optional<Value>)    : " << durA_exist << " ms, checksum=" << checksumA_exist << "\n";
    std::cout << "Version B (optional<ref_wrap>) : " << durB_exist << " ms, checksum=" << checksumB_exist << "\n";

    std::cout << "==== 不存在 key 测试 ====\n";
    std::cout << "Version A (optional<Value>)    : " << durA_non << " ms, checksum=" << checksumA_non << "\n";
    std::cout << "Version B (optional<ref_wrap>) : " << durB_non << " ms, checksum=" << checksumB_non << "\n";

    return 0;
}

编译运行后控制台输出如下:

ini 复制代码
==== 存在 key 测试 ====
Version A (optional<Value>)    : 2107 ms, checksum=5.16991e+06
Version B (optional<ref_wrap>) : 701 ms, checksum=5.16991e+06
==== 不存在 key 测试 ====
Version A (optional<Value>)    : 523 ms, checksum=0
Version B (optional<ref_wrap>) : 463 ms, checksum=0

可以看到,在查找存在的 key 的场景下,使用 std::reference_wrapper 后,由于避免了对 Value 的拷贝,有非常显著的性能提升。

相关推荐
楼田莉子1 小时前
C++算法题目分享:二叉搜索树相关的习题
数据结构·c++·学习·算法·leetcode·面试
大锦终2 小时前
【算法】模拟专题
c++·算法
Dontla2 小时前
Makefile介绍(Makefile教程)(C/C++编译构建、自动化构建工具)
c语言·c++·自动化
何妨重温wdys3 小时前
矩阵链相乘的最少乘法次数(动态规划解法)
c++·算法·矩阵·动态规划
重启的码农3 小时前
ggml 介绍 (6) 后端 (ggml_backend)
c++·人工智能·神经网络
重启的码农3 小时前
ggml介绍 (7)后端缓冲区 (ggml_backend_buffer)
c++·人工智能·神经网络
雨落倾城夏未凉3 小时前
5.通过拷贝构造函数复制一个对象,假如对象的成员中有个指针类型的变量,如何避免拷贝出来的副本中的该成员之下行同一块内存(等价于默认拷贝构造函数有没有缺点)
c++·后端
雨落倾城夏未凉3 小时前
4.深拷贝VS浅拷贝
c++·后端
tanyongxi664 小时前
C++ 特殊类设计与单例模式解析
java·开发语言·数据结构·c++·算法·单例模式