How can I improve the access time to this unordered map? If I instead allocate a 8 x 10 x 30 x 30 x 24000:
std::vector<std::vector<std::vector<std::vector<std::vector<float> > > > >
each access is about 0.0001 ms. Using an unordered_map
implementation, each access is about 0.001 ms, 10x slower. What can I do to reduce the access time for this unordered_map
? I'm doing a lot of insertions and modifications of existing entries.
#include <cstdio>
#include <random>
#include <unordered_map>
#include "boost/functional/hash.hpp"
#include "boost/date_time/posix_time/posix_time.hpp"
struct Key {
Key(int a, int b, int x, int y, int z) :
a_(a), b_(b), x_(x), y_(y), z_(z) {
}
bool operator==(const Key& other) const {
return (a_ == other.a_ &&
b_ == other.b_ &&
x_ == other.x_ &&
y_ == other.y_ &&
z_ == other.z_);
}
int a_, b_, x_, y_, z_;
};
namespace std {
template <>
struct hash<Key> {
typedef Key argument_type;
typedef std::size_t result_type;
result_type operator()(const Key& key) const {
std::size_t val = 0;
boost::hash_combine(val, key.a_);
boost::hash_combine(val, key.b_);
boost::hash_combine(val, key.x_);
boost::hash_combine(val, key.y_);
boost::hash_combine(val, key.z_);
return val;
}
};
} // namespace std.
int main(int argc, char** argv) {
std::default_random_engine generator;
std::uniform_int_distribution<int> random_z(1,24000);
std::uniform_real_distribution<float> random_vote(0.0, 1.0);
std::unordered_map<Key, float> votes;
int total_accesses = 0;
boost::posix_time::ptime start =
boost::posix_time::microsec_clock::local_time();
for (int i = 0; i < 200000; ++i) {
int z = random_z(generator); // z in [1,24000]
for (int a = 0; a < 8; ++a) {
for (int b = 0; b < 10; ++b) {
for (int x = 0; x < 30; ++x) {
for (int y = 0; y < 30; ++y) {
float this_vote = random_vote(generator);
Key key(a, b, x, y, z);
if (this_vote > 0.8) {
votes[key] += this_vote; // This is what is slow.
++total_accesses;
}
}
}
}
}
if ((i + 1) % 1000 == 0) {
boost::posix_time::ptime now =
boost::posix_time::microsec_clock::local_time();
boost::posix_time::time_duration diff = now - start;
printf("%d / %d : Milliseconds per access: %f\n",
i + 1, 200000,
static_cast<float>(diff.total_milliseconds()) / total_accesses);
}
}
}
I find it's amortized access time to be slow, and it takes a growing amount of memory (3.1GB after 4000 iterations of the main loop, 6.3GB after 8000 iterations, and 8.6GB after 12000 iterations):
1000 / 200000 : Milliseconds per access: 0.000548
2000 / 200000 : Milliseconds per access: 0.000653
3000 / 200000 : Milliseconds per access: 0.000682
4000 / 200000 : Milliseconds per access: 0.000834
5000 / 200000 : Milliseconds per access: 0.000874
6000 / 200000 : Milliseconds per access: 0.000926
7000 / 200000 : Milliseconds per access: 0.001107
8000 / 200000 : Milliseconds per access: 0.001143
9000 / 200000 : Milliseconds per access: 0.001187
10000 / 200000 : Milliseconds per access: 0.001234
11000 / 200000 : Milliseconds per access: 0.001285
12000 / 200000 : Milliseconds per access: 0.001338
Here is the version using the vector of vectors instead:
#include <cstdio>
#include <random>
#include <vector>
#include "boost/functional/hash.hpp"
#include "boost/date_time/posix_time/posix_time.hpp"
struct Key {
Key(int a, int b, int x, int y, int z) :
a_(a), b_(b), x_(x), y_(y), z_(z) {
}
bool operator==(const Key& other) const {
return (a_ == other.a_ &&
b_ == other.b_ &&
x_ == other.x_ &&
y_ == other.y_ &&
z_ == other.z_);
}
int a_, b_, x_, y_, z_;
};
namespace std {
template <>
struct hash<Key> {
typedef Key argument_type;
typedef std::size_t result_type;
result_type operator()(const Key& key) const {
std::size_t val = 0;
boost::hash_combine(val, key.a_);
boost::hash_combine(val, key.b_);
boost::hash_combine(val, key.x_);
boost::hash_combine(val, key.y_);
boost::hash_combine(val, key.z_);
return val;
}
};
} // namespace std.
int main(int argc, char** argv) {
std::default_random_engine generator;
std::uniform_int_distribution<int> random_z(1,24000);
std::uniform_real_distribution<float> random_vote(0.0, 1.0);
// This makes an 8 x 10 x 30 x 30 x 24000 vector of vectors... of vectors.
std::vector<std::vector<std::vector<std::vector<std::vector<float> > > > > votes;
for (size_t a = 0; a < 8; ++a) {
std::vector<std::vector<std::vector<std::vector<float> > > > a_grid;
for (size_t b = 0; b < 10; ++b) {
std::vector<std::vector<std::vector<float> > > b_grid;
for (size_t y = 0; y < 30; ++y) {
std::vector<std::vector<float> > y_grid;
for (size_t x = 0; x < 30; ++x) {
y_grid.push_back(std::vector<float>(24000, 0));
}
b_grid.push_back(y_grid);
}
a_grid.push_back(b_grid);
}
votes.push_back(a_grid);
}
int total_accesses = 0;
boost::posix_time::ptime start =
boost::posix_time::microsec_clock::local_time();
for (int i = 0; i < 200000; ++i) {
int z = random_z(generator); // z in [1,24000]
for (int a = 0; a < 8; ++a) {
for (int b = 0; b < 10; ++b) {
for (int x = 0; x < 30; ++x) {
for (int y = 0; y < 30; ++y) {
float this_vote = random_vote(generator);
if (this_vote > 0.8) {
votes[a][b][y][x][z] += this_vote;
++total_accesses;
}
}
}
}
}
if ((i + 1) % 1000 == 0) {
boost::posix_time::ptime now =
boost::posix_time::microsec_clock::local_time();
boost::posix_time::time_duration diff = now - start;
printf("%d / %d : Milliseconds per access: %f\n",
i + 1, 200000,
static_cast<float>(diff.total_milliseconds()) / total_accesses);
}
}
}
It takes 7.2GB of memory and reports constant, low (relative to unordered_map
) access time:
1000 / 200000 : Milliseconds per access: 0.000179
2000 / 200000 : Milliseconds per access: 0.000179
3000 / 200000 : Milliseconds per access: 0.000179
4000 / 200000 : Milliseconds per access: 0.000179
[z*8*10*30*30 + a*10*30*30 + b*30*30 + x*30 + y]
. It looks ugly, but it's fastest and straightest way in C++. \$\endgroup\$