【问题标题】:Optimizing time performances unordered_map c++优化时间性能 unordered_map c++
【发布时间】:2018-05-07 21:31:52
【问题描述】:

我陷入了优化问题。我有一个庞大的数据库(大约 1600 万个条目),它代表不同用户对不同项目给出的评级。从这个数据库中,我必须评估不同用户之间的相关性度量(即我必须实现一个相似度矩阵)。幸好这个相关矩阵是对称的,所以我只需要计算一半。

例如,让我关注矩阵的第一列:总共有 135k 用户,所以我保持一个用户固定,我找到了这个用户和所有其他用户之间的所有共同评分项目(使用 for 循环) .如果我将单个用户与 20k 其他用户而不是 135k 进行比较,也会出现时间问题。

我的方法如下:首先我查询数据库以获取例如前 20k 用户的所有数据(这也需要时间 indexes 实现,但它并没有打扰我,因为我只做一次),然后我使用 userID 作为键将所有内容存储在无序映射中;然后对于这个 unordered_map,我使用另一个 unordered_map 作为存储桶,它存储用户给出的所有评分,这次使用 itemID 作为键。

然后,为了找到都已评分的项目集,我在评分较少的用户上循环,搜索另一个用户是否也对相同的东西评分。我所知道的最快的数据结构是哈希图,但是对于单个完整的列,我的算法需要 30 秒(仅针对 20k 条目),这在 WEEKS 中转换为完整的矩阵。

代码如下:

void similarity_matrix(sqlite3 *db, sqlite3 *db_avg, sqlite3 *similarity, long int tot_users, long int interval) {

long int n = 1;
double sim;
string temp_s;
vector<string> insert_query;
sqlite3_stmt *stmt;

std::cout << "Starting creating similarity matrix..." << std::endl;

string query_string = "SELECT * from usersratings where usersratings.user <= 20000;";
unordered_map<int, unordered_map<int, int>> users_map = db_query(query_string.c_str(), db);
std::cout << "Query time: " << duration_ << " s." << std::endl;

unordered_map<int, int> u1_map = users_map[1];

string select_avg = "SELECT * from averages;";
unordered_map<int, double> avg_map = avg_value(select_avg.c_str(), db_avg);


for (int i = 2; i <= tot_users; i++)
{
    unordered_map<int, int> user;
    int compare_id;

    if (users_map[i].size() <= u1_map.size()) {
        user = users_map[i];
        compare_id = 1;
    }
    else {
        user = u1_map;
        compare_id = i;
    }

    int matches = 0;
    double newnum = 0;
    double newden1 = 0;
    double newden2 = 0;

    unordered_map<int, int> item_map = users_map[compare_id];
    for (unordered_map<int, int>::iterator it = user.begin(); it != user.end(); ++it)
    {
        if (item_map.size() != 0) {
            int rating = item_map[it->first];
            if (rating != 0) {
                double diff1 = (it->second - avg_map[1]);
                double diff2 = (rating - avg_map[i]);
                newnum += diff1 * diff2;
                newden1 += pow(diff1, 2);
                newden2 += pow(diff2, 2);
            }
        }

    }
    sim = newnum / (sqrt(newden1) * sqrt(newden2));
}

std::cout << "Execution time for first column: " << duration << " s." << std::endl;
std::cout << "First column finished..." << std::endl;

}

【问题讨论】:

  • 你不能让数据库来做这件事吗?要求数据库做一些工作可能会更快(sql可以做非常聪明的事情,特别是如果你使用存储过程和类似的语言如plpgsql等),而不是返回所有行并最终完成工作..只是一个想法..
  • 这在Code Review上会更好

标签: c++ performance sqlite time


【解决方案1】:

这对我来说是一个直接的潜在性能陷阱:

unordered_map<int, unordered_map<int, int>> users_map = db_query(query_string.c_str(), db);

如果每个用户的每个子图的大小与用户数量接近,那么您就有了一个二次复杂度算法,当您拥有的用户越多,该算法的速度就越慢。

unordered_map 确实提供恒定时间搜索,但它仍然是一种搜索。执行此操作所需的指令量将使索引数组的成本相形见绌,特别是如果有很多冲突,这意味着每次尝试搜索地图时都会出现内部循环。它也不一定以允许最快顺序迭代的方式表示。因此,如果您至少可以将std::vector 用于子列表并像这样使用avg_map,那对初学者来说应该会有很大帮助:

typedef pair<int, int> ItemRating;
typedef vector<ItemRating> ItemRatings;
unordered_map<int, ItemRatings> users_map = ...;
vector<double> avg_map = ...;

即使外部users_map 也可能是vector,除非它是稀疏的并且并非所有索引都被使用。如果它是稀疏的并且用户 ID 的范围仍然适合合理的范围(不是天文数字的大整数),您可能会构造两个向量 - 一个存储用户数据并且大小与用户数量成正比,而另一个与用户的有效索引范围成正比,并且仅将索引存储到前一个向量中,以便通过简单的数组查找将用户 ID 转换为索引,如果您需要能够通过用户 ID 访问用户数据。

// User data array.
vector<ItemRatings> user_data(num_users);

// Array that translates sparse user ID integers to indices into the 
// above dense array. A value of -1 indicates that a user ID is not used.
// To fetch user data for a particular user ID, we do: 
// const ItemRatings& ratings = user_data[user_id_to_index[user_id]];
vector<int> user_id_to_index(biggest_user_index+1, -1);

对于外部循环的每次迭代,您还不必要地复制了那些 unordered_maps。虽然我不认为这是最大瓶颈的根源,但它有助于避免通过使用引用或指针来深度复制这些您甚至不修改的数据结构:

// Shallow copy, don't deep copy big stuff needlessly.
const unordered_map<int, int>& user = users_map[i].size() <= u1_map.size() ?                                     
                                      users_map[i]: u1_map;
const int compare_id = users_map[i].size() <= u1_map.size() ? 1: i;
const unordered_map<int, int>& item_map = users_map[compare_id];
...

您也不需要在内部循环中检查item_map 是否为空。那张支票应该挂在外面。这是一种微优化,可能根本没有多大帮助,但仍能消除明显的浪费。

第一次通过后的最终代码将是这样的:

vector<ItemRatings> user_data = ..;
vector<double> avg_map = ...;

// Fill `rating_values` with the values from the first user.
vector<int> rating_values(item_range, 0);
const ItemRatings& ratings1 = user_data[0];
for (auto it = ratings1.begin(); it != ratings1.end(); ++it)
{
    const int item = it->first;
    const int rating = it->second;
    rating_values[item] += rating;
}

// For each user starting from the second user:
for (int i=1; i < tot_users; ++i)
{
    double newnum = 0;
    double newden1 = 0;
    double newden2 = 0;

    const ItemRatings& ratings2 = user_data[i];
    for (auto it = ratings2.begin(); it != ratings2.end(); ++it)
    {
        const int item = it->first;
        const int rating1 = rating_values[it->first];
        if (rating != 0) {
            const int rating2 = it->second;
            double diff1 = rating2 - avg_map[1];
            double diff2 = rating1 - avg_map[i];
            newnum += diff1 * diff2;
            newden1 += pow(diff1, 2);
            newden2 += pow(diff2, 2);
        }
    }
    sim = newnum / (sqrt(newden1) * sqrt(newden2));
}

上述代码最大的不同是我们消除了所有通过unordered_map 进行的搜索,并将它们替换为对数组的简单索引访问。我们还消除了很多数据结构的冗余复制。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-12-08
    • 1970-01-01
    • 2013-06-10
    • 2017-10-31
    • 1970-01-01
    • 2015-08-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多