Skip to content

Commit e547469

Browse files
kelbonkelbon
authored andcommitted
encode with cache + dyntab find
1 parent 8e847b5 commit e547469

4 files changed

Lines changed: 301 additions & 42 deletions

File tree

include/hpack/dynamic_table.hpp

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
#include <memory_resource>
44

5-
#include <boost/intrusive/set.hpp>
5+
#include <boost/intrusive/unordered_set.hpp>
66

77
#include "hpack/basic_types.hpp"
88
#include "hpack/static_table.hpp"
@@ -16,15 +16,33 @@ struct dynamic_table_t {
1616

1717
private:
1818
struct key_of_entry {
19-
using type = table_entry;
20-
table_entry operator()(const entry_t& v) const noexcept;
19+
using type = std::string_view;
20+
std::string_view operator()(const entry_t& v) const noexcept;
21+
};
22+
struct equal_by_namevalue {
23+
bool operator()(const key_of_entry::type& l, const key_of_entry::type& r) const noexcept {
24+
return l == r;
25+
}
26+
};
27+
struct hash_by_namevalue {
28+
size_t operator()(key_of_entry::type) const noexcept;
2129
};
22-
// for forward declaring entry_t
23-
using hook_type_option = bi::base_hook<bi::set_base_hook<bi::link_mode<bi::normal_link>>>;
2430

2531
// invariant: do not contain nullptrs
2632
std::vector<entry_t*> entries;
27-
bi::multiset<entry_t, bi::constant_time_size<false>, hook_type_option, bi::key_of_value<key_of_entry>> set;
33+
using entry_set_hook = bi::unordered_set_base_hook<bi::link_mode<bi::normal_link>, bi::store_hash<true>,
34+
bi::optimize_multikey<true>>;
35+
// hashed by name
36+
using entry_set_t = bi::unordered_multiset<entry_t, bi::base_hook<entry_set_hook>,
37+
bi::key_of_value<key_of_entry>, bi::equal<equal_by_namevalue>,
38+
bi::hash<hash_by_namevalue>, bi::power_2_buckets<true>>;
39+
40+
static constexpr inline size_t initial_buckets_count = 4;
41+
42+
// Note: must be before `set` because of destroy ordering
43+
// invariant: .size is always pow of 2
44+
std::vector<entry_set_t::bucket_type> buckets;
45+
entry_set_t set;
2846
// in bytes
2947
// invariant: <= _max_size
3048
size_type _current_size = 0;
@@ -49,7 +67,9 @@ struct dynamic_table_t {
4967
Insertion Point Dropping Point
5068
*/
5169
public:
52-
dynamic_table_t() = default;
70+
// 4096 - default size by protocol
71+
dynamic_table_t() : dynamic_table_t(4096) {
72+
}
5373
// `user_protocol_max_size` and `max_size()` both initialized to `max_size`
5474
explicit dynamic_table_t(size_type max_size,
5575
std::pmr::memory_resource* m = std::pmr::get_default_resource()) noexcept;
@@ -97,11 +117,15 @@ struct dynamic_table_t {
97117
return entries.size() + static_table_t::first_unused_index - 1;
98118
}
99119

120+
// searches both static and dynamic table
100121
find_result_t find(std::string_view name, std::string_view value) noexcept;
122+
// searches both static and dynamic table
123+
// precondition: name <= current_max_index()
101124
find_result_t find(index_type name, std::string_view value) noexcept;
102125

103-
// precondition: first_unused_index <= index <= current_max_index()
126+
// precondition: 0 < index <= current_max_index()
104127
// Note: returned value may be invalidated on next .add_entry()
128+
// searches both in static and dynamic tables
105129
table_entry get_entry(index_type index) const noexcept;
106130

107131
void reset() noexcept;

include/hpack/encoder.hpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include "hpack/strings.hpp"
55
#include "hpack/integers.hpp"
66

7+
#include <charconv>
8+
79
namespace hpack {
810

911
struct encoder {
@@ -37,6 +39,11 @@ struct encoder {
3739

3840
// only name indexed
3941
// precondition: header_index present in static or dynamic table
42+
//
43+
// Note: will encode to the cached header, but calling this function again will not result in more efficient
44+
// encoding.
45+
// Instead, it will send requests to cache the header and this will result in evicting from dynamic table.
46+
// Its likely you want to use encode_with_cache instead
4047
template <bool Huffman = false, Out O>
4148
O encode_header_and_cache(index_type header_index, std::string_view value, O _out) {
4249
assert(header_index <= dyntab.current_max_index() && header_index != 0);
@@ -51,6 +58,11 @@ struct encoder {
5158

5259
// indexes value for future use
5360
// 'out_index' contains index of 'name' + 'value' pair after encode
61+
//
62+
// Note: will encode to the cached header, but calling this function again will not result in more efficient
63+
// encoding.
64+
// Instead, it will send requests to cache the header and this will result in evicting from dynamic table.
65+
// Its likely you want to use encode_with_cache instead
5466
template <bool Huffman = false, Out O>
5567
O encode_header_and_cache(std::string_view name, std::string_view value, O _out) {
5668
/*
@@ -76,6 +88,34 @@ struct encoder {
7688
return noexport::unadapt<O>(encode_string<Huffman>(value, out));
7789
}
7890

91+
// encodes header like 'encode_header_and_cache', but uses created cache, so next calls much more efficient
92+
// than first call
93+
//
94+
// Note: does not use static_table. In this case its better to use `encode_header_fully_indexed`
95+
template <bool Huffman = false, Out O>
96+
O encode_with_cache(std::string_view name, std::string_view value, O out) {
97+
find_result_t r = dyntab.find(name, value);
98+
if (r.value_indexed) [[likely]] {
99+
// its likely, because only first call will be not cached
100+
return encode_header_fully_indexed(r.header_name_index, out);
101+
}
102+
return encode_header_and_cache<Huffman>(name, value, out);
103+
}
104+
105+
// encodes header like 'encode_header_and_cache', but uses created cache, so next calls much more efficient
106+
// than first call
107+
//
108+
// Note: does not use static_table. In this case its better to use `encode_header_fully_indexed`
109+
template <bool Huffman = false, Out O>
110+
O encode_with_cache(index_type name, std::string_view value, O out) {
111+
find_result_t r = dyntab.find(name, value);
112+
if (r.value_indexed) [[likely]] {
113+
// its likely, because only first call will be not cached
114+
return encode_header_fully_indexed(r.header_name_index, out);
115+
}
116+
return encode_header_and_cache<Huffman>(name, value, out);
117+
}
118+
79119
template <bool Huffman = false, Out O>
80120
O encode_header_without_indexing(index_type name, std::string_view value, O _out) {
81121
/*
@@ -251,6 +291,35 @@ struct encoder {
251291
dyntab.update_size(new_size);
252292
return it;
253293
}
294+
295+
// encodes :status pseudoheader for server
296+
// precondition:
297+
template <Out O>
298+
O encode_status(int status, O out) {
299+
using enum static_table_t::values;
300+
switch (status) {
301+
case 200:
302+
return encode_header_fully_indexed(status_200, out);
303+
case 204:
304+
return encode_header_fully_indexed(status_204, out);
305+
case 206:
306+
return encode_header_fully_indexed(status_206, out);
307+
case 304:
308+
return encode_header_fully_indexed(status_304, out);
309+
case 400:
310+
return encode_header_fully_indexed(status_400, out);
311+
case 404:
312+
return encode_header_fully_indexed(status_404, out);
313+
case 500:
314+
return encode_header_fully_indexed(status_500, out);
315+
default:
316+
char data[32];
317+
auto [ptr, ec] = std::to_chars(data, data + 32, status);
318+
assert(ec == std::errc{});
319+
// its likely, that server will send this status again, so cache it
320+
return encode_with_cache(status_200, std::string_view(+data, ptr), out);
321+
}
322+
}
254323
};
255324

256325
} // namespace hpack

src/dynamic_table.cpp

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
#include <utility>
55
#include <cstring> // memcpy
66

7-
namespace bi = boost::intrusive;
8-
97
namespace hpack {
108

11-
struct dynamic_table_t::entry_t : bi::set_base_hook<bi::link_mode<bi::normal_link>> {
9+
struct dynamic_table_t::entry_t : entry_set_hook {
1210
const size_type name_end;
1311
const size_type value_end;
1412
const size_t _insert_c;
@@ -46,6 +44,27 @@ struct dynamic_table_t::entry_t : bi::set_base_hook<bi::link_mode<bi::normal_lin
4644
}
4745
};
4846

47+
std::string_view dynamic_table_t::key_of_entry::operator()(const dynamic_table_t::entry_t& v) const noexcept {
48+
return v.name();
49+
}
50+
51+
static size_t hash_calc(std::string_view bytes) noexcept {
52+
// standard hash is bad sometimes
53+
constexpr uint64_t fnv_offset_basis = 14695981039346656037ULL;
54+
constexpr uint64_t fnv_prime = 1099511628211ULL;
55+
size_t hash = fnv_offset_basis;
56+
for (auto byte : bytes) {
57+
hash ^= static_cast<uint64_t>(byte);
58+
hash *= fnv_prime;
59+
}
60+
return hash;
61+
}
62+
63+
size_t dynamic_table_t::hash_by_namevalue::operator()(
64+
dynamic_table_t::key_of_entry::type str) const noexcept {
65+
return hash_calc(str);
66+
}
67+
4968
// precondition: 'e' now in entries
5069
index_type dynamic_table_t::indexof(const dynamic_table_t::entry_t& e) const noexcept {
5170
return static_table_t::first_unused_index + (_insert_count - e._insert_c);
@@ -61,12 +80,10 @@ static size_type entry_size(const dynamic_table_t::entry_t& entry) noexcept {
6180
return entry.value_end + 32;
6281
}
6382

64-
table_entry dynamic_table_t::key_of_entry::operator()(const dynamic_table_t::entry_t& v) const noexcept {
65-
return {v.name(), v.value()};
66-
}
67-
6883
dynamic_table_t::dynamic_table_t(size_type max_size, std::pmr::memory_resource* m) noexcept
69-
: _current_size(0),
84+
: buckets(initial_buckets_count),
85+
set({buckets.data(), buckets.size()}),
86+
_current_size(0),
7087
_max_size(max_size),
7188
_user_protocol_max_size(max_size),
7289
_insert_count(0),
@@ -109,6 +126,14 @@ index_type dynamic_table_t::add_entry(std::string_view name, std::string_view va
109126
evict_until_fits_into(_max_size - new_entry_size);
110127
entries.push_back(entry_t::create(name, value, ++_insert_count, _resource));
111128
set.insert(*entries.back());
129+
if (entries.size() > buckets.size() / 2) {
130+
// https://github.com/boostorg/intrusive/issues/96
131+
// workaround:
132+
// unordered set bucket copy(move) ctor does nothing, so .resize will be UB
133+
decltype(buckets) new_buckets(buckets.size() * 2);
134+
set.rehash({new_buckets.data(), new_buckets.size()});
135+
buckets = std::move(new_buckets);
136+
}
112137
_current_size += new_entry_size;
113138
return static_table_t::first_unused_index;
114139
}
@@ -131,36 +156,26 @@ void dynamic_table_t::update_size(size_type new_max_size) {
131156

132157
find_result_t dynamic_table_t::find(std::string_view name, std::string_view value) noexcept {
133158
find_result_t r;
134-
auto it = set.find(table_entry{name, value});
135-
if (it == set.end())
136-
return r;
137-
if (name == it->name()) {
138-
r.header_name_index = indexof(*it);
139-
if (value == it->value())
140-
r.value_indexed = true;
159+
auto i = set.bucket(name);
160+
auto b = set.begin(i);
161+
auto e = set.end(i);
162+
for (; b != e; ++b) {
163+
if (b->name() == name) {
164+
r.header_name_index = indexof(*b);
165+
if (b->value() == value) {
166+
r.value_indexed = true;
167+
return r;
168+
}
169+
}
141170
}
142171
return r;
143172
}
144173

145174
find_result_t dynamic_table_t::find(index_type name, std::string_view value) noexcept {
146175
assert(name <= current_max_index());
147-
find_result_t r;
148-
if (name < static_table_t::first_unused_index || name > current_max_index() || name == 0)
149-
return r;
150-
table_entry e = get_entry(name);
151-
if (e.value == value) {
152-
r.header_name_index = name;
153-
r.value_indexed = true;
154-
return r;
155-
}
156-
auto it = set.find(table_entry{e.name, value});
157-
assert(it != set.end());
158-
if (e.name == it->name()) {
159-
r.header_name_index = indexof(*it);
160-
if (value == it->value())
161-
r.value_indexed = true;
162-
}
163-
return r;
176+
if (name == 0) [[unlikely]]
177+
return {};
178+
return find(get_entry(name).name, value);
164179
}
165180

166181
void dynamic_table_t::reset() noexcept {
@@ -175,15 +190,17 @@ void dynamic_table_t::evict_until_fits_into(size_type bytes) noexcept {
175190
size_type i = 0;
176191
for (; _current_size > bytes; ++i) {
177192
_current_size -= entry_size(*entries[i]);
178-
set.erase(set.s_iterator_to(*entries[i]));
193+
set.erase(set.iterator_to(*entries[i]));
179194
entry_t::destroy(entries[i], _resource);
180195
}
181196
// evicts should be rare operation
182197
entries.erase(entries.begin(), entries.begin() + i);
183198
}
184199

185200
table_entry dynamic_table_t::get_entry(index_type index) const noexcept {
186-
assert(index >= static_table_t::first_unused_index && index <= current_max_index());
201+
assert(index != 0 && index <= current_max_index());
202+
if (index < static_table_t::first_unused_index)
203+
return static_table_t::get_entry(index);
187204
auto& e = *(&entries.back() - (index - static_table_t::first_unused_index));
188205
return table_entry{e->name(), e->value()};
189206
}

0 commit comments

Comments
 (0)