游戏排行榜快速实现

pre

  • 之前的排行榜实现为 std::vector<rank_info> + std::unordered_map<player_id, rank_index>
    • 问题1:每次更新排行榜需要全量排序,因为容器存储了具体角色的排名
    • 问题2:全量排序后需要全量存储
    • 没有引发灾难性的问题是因为排行榜最大尺寸只有100
  • 取舍:是否需要存储具体玩家所在排行榜的具体位置?
    • 当前游戏更多的操作是查看排行榜榜单,榜单数据无论是反序列化或者遍历拼ui的动作上都可以获得我具体的排名
    • 需要指定玩家具体排名数据是可以遍历榜单,时间复杂度O(N) 相比全量排序O(N^2)还是小的
    • 遍历的方式还可以优化(todo)

快速重构

  • 最简单的方式 std::set
  • set::iterator 是稳定的,只有erase动作有影响 https://en.cppreference.com/w/cpp/container#Iterator_invalidation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    #pragma once
    #include <map>
    #include <string>
    #include <unordered_map>
    #include <unordered_set>

    namespace rank {

    class container_interface {
    public:
    virtual ~container_interface() = default;
    // todo:
    // virtual std::string serialize() = 0;
    // virtual std::string unserialize() = 0;
    };

    template <std::size_t count_vv, class sort_key_tt, class element_tt,
    class element_key_tt, class compare_tt = std::less<sort_key_tt> >
    class container : public container_interface {
    using sort_data =
    std::map<sort_key_tt, std::pair<element_key_tt, element_tt>, compare_tt>;
    using sort_data_iterator = typename sort_data::iterator;

    private:
    const std::size_t _count = count_vv;
    sort_data _data;
    std::unordered_map<element_key_tt, sort_data_iterator> _elements;
    std::unordered_set<element_key_tt> _dirty_elements;

    public:
    container() = default;

    container(compare_tt&& compare_call) : _data(compare_call) {}

    ~container() override = default;

    [[maybe_unused]] bool insert(const sort_key_tt& skey, const element_tt& ev, const element_key_tt& ekey) {
    remove(ekey);

    if (auto [data_it, res] = _data.emplace(skey, std::make_pair(ekey, ev));
    res) {
    if (auto [ele_it, res] = _elements.emplace(ekey, data_it); res) {
    _dirty_elements.emplace(ekey);
    }
    }

    while (_data.size() > _count) {
    auto last_data_it = std::prev(_data.end());
    remove(last_data_it->second.first);
    }

    return exist(ekey);
    }

    [[maybe_unused]] bool remove(const element_key_tt& ekey) {
    if (const auto it = _elements.find(ekey); it != _elements.end()) {
    _dirty_elements.emplace(ekey);

    _data.erase(it->second);
    _elements.erase(it);
    return true;
    }
    return false;
    }

    bool exist(const element_key_tt& ekey) const {
    return _elements.contains(ekey);
    }
    };

    }; // namespace rank
  • 测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    namespace rank_test {

    struct sort_key {
    uint32_t level = 0;
    uint32_t exp = 0;

    uint64_t ts = 0;
    uint64_t auto_increment = 0;
    };

    using element_key = uint64_t;

    struct element_value {
    std::string name;
    };
    }; // namespace rank_test

    template <>
    struct std::less<rank_test::sort_key> {
    bool operator()(const rank_test::sort_key& left,
    const rank_test::sort_key& right) const {
    return left.level < right.level ||
    (left.level == right.level && left.exp < right.exp) ||
    (left.level == right.level && left.exp == right.exp &&
    left.ts < right.ts) ||
    (left.level == right.level && left.exp == right.exp &&
    left.ts == right.ts && left.auto_increment < right.auto_increment);
    }
    };
    template <>
    struct std::greater<rank_test::sort_key> {
    bool operator()(const rank_test::sort_key& left,
    const rank_test::sort_key& right) const {
    return left.level > right.level ||
    (left.level == right.level && left.exp > right.exp) ||
    (left.level == right.level && left.exp == right.exp &&
    left.ts > right.ts) ||
    (left.level == right.level && left.exp == right.exp &&
    left.ts == right.ts && left.auto_increment > right.auto_increment);
    }
    };

    rank::container<10, rank_test::sort_key, rank_test::element_value,
    rank_test::element_key>
    rc;

    rank::container<10, rank_test::sort_key, rank_test::element_value,
    rank_test::element_key, std::greater<rank_test::sort_key>>
    rcg;

    for (int i = 0; i < 100; ++i) {
    rank_test::sort_key sk{1, 1, 1, i};
    rank_test::element_value ev{"name_" + std::to_string(i)};

    rc.insert(sk, ev, i);
    rcg.insert(sk, ev, i);
    }

至此都很美好,一个标准可用的排序容器

  • 新需求:lua 需要决定用哪些属性排序,并指定具体属性的顺序
  • 第一反应是:假设用 level, exp 双属性排序,想要得到 等级相同,经验大的排前面的样子,可以: level << 32 | exp 得到一个新值作为 sort_key
  • 然而这种方式…策划不懂(或者只是觉得算着麻烦…)
  • 策划提出了想要的样子: 构建排行榜时给定 table[“level”] = 升序/降序; table[“exp”] = 升序/降序
  • 还是要实现….
  • 一个排序规则数据而已,但是容器本身只有 sort_key 的类型,排序是需要 operator < 函数处理的…
  • 并且这里实现的 container 想作为一个单纯的容器使用,并不想引入外部特殊规则….只能想办法把数据引入到 compare_tt 里
  • 数据可以作为lambda的闭包: operator < 使用lambda
  • https://stackoverflow.com/questions/72673837/c11-how-to-use-lambda-as-type-parameter-where-it-requires-a-functor-type-l
  • 于是就有了:container(compare_tt&& compare_call) : _data(compare_call) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace lua_rank_test {

struct sort_key {
std::unordered_map<std::string, int64_t> keys;
};

}; // namespace lua_rank_test

std::vector<std::pair<std::string, bool>> compares{{"level", true},
{"exp", true}};
auto lua_comp = [compares](const lua_rank_test::sort_key& left,
const lua_rank_test::sort_key& right) {
for (const auto& one : compares) {
auto lv = left.keys.at(one.first);
auto rv = right.keys.at(one.first);
if (lv == rv)
continue;
if (one.second) // sort down
return lv > rv;
return lv < rv;
}
return false;
};

rank::container<10, rank_test::sort_key, rank_test::element_value,
rank_test::element_key, decltype(lua_comp)>
rcl(lua_comp);
  • 明显有性能问题,但是先能跑吧

set comparator exception

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void test_set() {
struct info {
uint32_t id = 0;
uint32_t seq = 0;
uint32_t count = 0;

bool operator < (const info& src) const {
return count <= src.count || seq <= src.seq;
}
};

std::set<info> set_info;
set_info.emplace(info{1, 1, 1});
set_info.emplace(info{1, 1, 1});
}

// msvc环境:运行这段代码在第二个emplace处会抛出异常 invalid comparator
// linux g++ 环境:暂时未发现问题
// msvc 严格要求 1. 比较运算符不能因为插入顺序产生歧义 2. 相等的比较必须返回 false
// 举例:先插入 {1, 2, 1};

// https://hankin2015.github.io/2017/12/08/20171208SetBug/

// 准确的 operator <
bool operator < (const info& src) const {
return count < src.count
|| (count == src.count && seq < src.seq) // 如果不希望 seq 参与比较可不要
|| (count == src.count && seq == src.seq && id < src.id) // 如果不希望 seq & id 参与比较可不要
;
}
------ 本文结束 ------
------ 版权声明:转载请注明出处 ------