#c #python-3.x #dictionary #hash #hashtable
Вопрос:
У меня есть словарь Python, ключи которого представляют собой строки, состоящие из строчных букв английского алфавита, а значения-int. Более того, существует ровно 5e6 уникальных ключей, все они представляют собой строки длиной ровно 10. Удивительно, но поиск не занимает много времени. Я ожидал, что выполнение займет около 4 или более секунд, но оно не превышает 2,5 с.
Я преобразовал код Python в C , по аналогии со словарем a map
. Я попробовал с map
, unordered_map
и gp_hash_table
, в то время как все они занимали более 2 секунд в C .
Вот генератор, который я использовал для генерации уникальных строк.
from sys import stdout
def increment(l):
n = len(l)
i = n - 1
while i >= 0 and l[i] == 'z':
l[i] = 'a'
i -= 1
l[i] = chr(ord(l[i]) 1)
print(5 * 10**6)
string = ['a' for i in range(10)]
for i in range(5 * 10**6):
stdout.write(''.join(string) 'n')
increment(string)
string = ['a' for i in range(10)]
print(10**6)
for i in range(5 * 10**6):
stdout.write(''.join(string) 'n')
increment(string)
Выходные данные этой программы сохраняются в файле, названном Strings.txt
с помощью команды python3 Test.py > Strings.txt
., после чего файл Strings.txt
будет выглядеть следующим образом.
5000000
aaaaaaaaaa
aaaaaaaaab
aaaaaaaaac
aaaaaaaaad
aaaaaaaaae
aaaaaaaaaf
...
...
...
aaaaakymlr
1000000
aaaaaaaaaa
aaaaaaaaab
aaaaaaaaac
...
...
...
aaaaakymlq
aaaaakymlr
Ниже приведен код CPP, на который я ссылался в приведенном выше контексте.
#include <bits/stdc .h>
#include <ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N = 0;
cin >> N;
map<string, int> freq;
// unordered_map<string, int> freq;
// gp_hash_table<string, int> freq;
for(int i = 0; i < N; i ) {
string s;
cin >> s;
freq[s] ;
}
int Q = 0;
cin >> Q;
for(int i = 0; i < Q; i ) {
string s;
cin >> s;
cout << freq[s] << 'n';
}
return 0;
}
And the following is the Python3 Code I used.
from collections import defaultdict
from sys import stdin, stdout
input = stdin.readline
freq = defaultdict(int)
for i in range(int(input())):
freq[input()] = 1
for i in range(int(input())):
stdout.write(str(freq[input()]) 'n')
Here are the results when the above codes are executed.
suman@Skynet:~/Documents/String_Pairs$ python3 Test.py > Strings.txt
suman@Skynet:~/Documents/String_Pairs$ time python3 Dict.py < Strings.txt > P_out.txt
real 0m3.145s
user 0m2.662s
sys 0m0.164s
suman@Skynet:~/Documents/String_Pairs$ time python3 Dict.py < Strings.txt > P_out.txt
real 0m2.772s
user 0m2.568s
sys 0m0.204s
suman@Skynet:~/Documents/String_Pairs$ g -o exe Map.cpp -O2 -std=c 17
suman@Skynet:~/Documents/String_Pairs$ time ./exe < Strings.txt > Cpp_out.txt
real 0m2.346s
user 0m2.265s
sys 0m0.080s
suman@Skynet:~/Documents/String_Pairs$ time ./exe < Strings.txt > Cpp_out.txt
real 0m2.513s
user 0m2.417s
sys 0m0.096s
suman@Skynet:~/Documents/String_Pairs$ g -o exe Unordered_Map.cpp -O2 -std=c 17
suman@Skynet:~/Documents/String_Pairs$ time ./exe < Strings.txt > Cpp_out.txt
real 0m2.769s
user 0m2.660s
sys 0m0.108s
suman@Skynet:~/Documents/String_Pairs$ time ./exe < Strings.txt > Cpp_out.txt
real 0m2.806s
user 0m2.690s
sys 0m0.116s
suman@Skynet:~/Documents/String_Pairs$ g -o exe gp_hash_table.cpp -O2 -std=c 17
suman@Skynet:~/Documents/String_Pairs$ time ./exe < Strings.txt > Cpp_out.txt
real 0m2.099s
user 0m1.686s
sys 0m0.412s
suman@Skynet:~/Documents/String_Pairs$ time ./exe < Strings.txt > Cpp_out.txt
real 0m2.009s
user 0m1.605s
sys 0m0.404s
suman@Skynet:~/Documents/String_Pairs$
Now, the only thing I am concerned about is the fact that Python3 is 5x slower than CPP but still, it’s Competing with CPP when working with hash tables.
Is there any way I can beat the time complexity of the Python hash table?
Any help is greatly appreciated.
UPDATE:
Here’s an update, that excludes time taken for reading strings.
#include <bits/stdc .h>
#include <ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
using namespace chrono;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N = 0;
cin >> N;
vector<string> words(N);
for(int i = 0; i < N; i ) {
cin >> words[i];
}
// map<string, int> freq;
// unordered_map<string, int> freq;
gp_hash_table<string, int> freq;
auto start = high_resolution_clock::now();
for(string word : words) {
freq[word] ;
}
auto end = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(end - start);
cout << duration.count() / 1e6 << 'n';
int Q = 0;
cin >> Q;
vector<string> queries(Q);
for(int i = 0; i < Q; i ) {
cin >> queries[i];
}
vector<int> results(Q);
start = high_resolution_clock::now();
for(int i = 0; i < Q; i ) {
results[i] = freq[queries[i]];
}
end = high_resolution_clock::now();
duration = duration_cast<microseconds>(end - start);
cout << duration.count() / 1e6 << 'n';
for(int i = 0; i < Q; i ) {
cout << results[i] << 'n';
}
return 0;
}
The Python Code
from collections import defaultdict
from time import time
from sys import stdin, stdout
input = stdin.readline
freq = defaultdict(int)
strings = []
for i in range(int(input())):
strings.append(input())
start = time()
for string in strings:
freq[string] = 1
end = time()
print("%.4f" %(end - start))
queries = []
output = []
for i in range(int(input())):
queries.append(input())
start = time()
for query in queries:
output.append(freq[query])
end = time()
print("%.4f" %(end - start))
stdout.write('n'.join(map(str, output)))
Even now, Python is running faster than CPP.
The results:
Cpp_out.txt (the time taken for map
, unordered_map
and gp_hash_table
— all are greater than 2s).
2.28297
0.109844
1
1
...
...
...
P_out.txt
1.7818
0.1977
1
1
...
...
UPDATE 2: I have modified the code so that I don’t include time taken for reading or writing as well as using references everywhere. Now it is kinda acceptable that CPP
beats Python3
in hashing. Here are the benchmarks.
// CPP Code
#include <bits/stdc .h>
#include <ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
using namespace chrono;
struct djb2 {
unsigned long operator()(const stringamp; str) const {
unsigned long hash = 5381;
for (auto c : str)
hash = ((hash << 5) hash) c;
return hash;
}
};
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N = 0;
cin >> N;
vector<string> words(N);
for(int i = 0; i < N; i ) {
cin >> words[i];
}
// map<string, int> freq;
// unordered_map<string, int> freq;
gp_hash_table<string, int> freq;
auto start = high_resolution_clock::now();
for(const string amp;word : words) {
freq[word] ;
}
auto end = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(end - start);
cout << duration.count() / 1e6 << 'n';
int Q = 0;
cin >> Q;
vector<string> queries(Q);
for(int i = 0; i < Q; i ) {
cin >> queries[i];
}
vector<int> results(Q);
start = high_resolution_clock::now();
for(int i = 0; i < Q; i ) {
results[i] = freq[queries[i]];
}
end = high_resolution_clock::now();
duration = duration_cast<microseconds>(end - start);
cout << duration.count() / 1e6 << 'n';
for(int i = 0; i < Q; i ) {
cout << results[i] << 'n';
}
return 0;
}
# Python3 Code
from collections import defaultdict
from time import time
from sys import stdin, stdout
input = stdin.readline
freq = defaultdict(int)
strings = []
for i in range(int(input())):
strings.append(input())
start = time()
for string in strings:
freq[string] = 1
end = time()
print("%.4f" %(end - start))
queries = []
output = []
for i in range(int(input())):
queries.append(input())
start = time()
for query in queries:
output.append(freq[query])
end = time()
print("%.4f" %(end - start))
stdout.write('n'.join(map(str, output)))
Cpp_out.txt
1.60026
0.071471
P_out.txt
1.7849
0.1987
Итак, понятно, что CPP's
gp_hash_table
превосходит Python3's
хэш-таблицу.
Я прошел через реализацию хэш-таблицы Python3. Они используют что-то под названием СИФАШ. Я хочу генерировать строки таким образом, чтобы количество столкновений при хэшировании строк было максимальным. Это что-то вроде атаки на столкновение, но я хочу, чтобы по крайней мере уникальные строки стоимостью 5000 долларов США создавали один и тот же хэш.
Может ли кто-нибудь предоставить для этого какой-либо ресурс (обратите внимание, что мне нужны уникальные строки стоимостью не менее 5000 долларов США, которые создают один и тот же хэш).
Комментарии:
1. Что же
Unordered_Map.cpp
в нем содержится?2. @kiner_shah Содержит точно такой же скрипт
Map.cpp
, с той лишь разницейunordered_map<string, int>
, что он используется вместо двух других (они прокомментированы).3. Вы включаете время на чтение и запись > 50 мегабайт. Результаты по существу не имеют ничего общего с производительностью хэш-таблицы.
4. @ChitturiSaiSuman Ваш обновленный код повторяет
string
элементы по значению, что означает, что он должен выполнять динамическое распределение и копировать все содержимое. Вы должны делатьfor (const string amp;word : words)
(илиconst auto amp;word
)5. Вы должны возглавить свою карту C , то есть это .
Ответ №1:
reserve
Вызов, указанный в комментарии, является наиболее важным для unordered_map
. Кроме того, вы должны использовать правильную реализацию unordered_map
(не стандартную для g , с 2019 года все в порядке) и учитывать используемую вами архитектуру.
С помощью инструментов Microsoft (Python37_64 и VS 2019) на моем компьютере — с использованием x86, за исключением последних строк:
- Python 1.697 0.202
- Оригинальная карта C : 1.361 0.138
- Оригинальная C неупорядоченная карта: 1.035 0.067
- строка constamp;: 1.013 0.067
- резерв: 0,675 0,056
- На x64 вместо этого: 0,686 0,060 (так что почти то же самое)
Использование robin_hood (как предлагает @thc ) не быстрее на x86, прежде чем вы добавите резерв — и даже в этом случае разница не столь существенна для неупорядоченной карты gcc:
- robin_hood неупорядоченная карта x86: 1.107 0.100
- строка const и x86: 1.059 0.101
- резервный код: 0,477 0,108
Однако robin_hood также быстрее в построении, но не в поиске, если вы работаете на x64 (и резерв все еще важен).:
- Оригинал ПРОТИВ C unordered_map x64: 1.130 0.065
- строка const и x64: 1,119 0,063
- резервный код: 0,671 0,065
- robin_hood неупорядоченная карта x64: 0,611 0,069
- строка const и x64: 0,577 0,072
- резервный код: 0,384 0,069
- Оригинальный g C unordered_map x64: 1.639 0.135
- строка const и x64: 1.611 0.135
- резервная копия: 0,946 0,128
Фрагмент кода (обратите внимание, что важно, чтобы резерв был внутри времени для справедливости):
auto start = high_resolution_clock::now();
unordered_map<string, int> freq;
freq.reserve(Q);
for(const stringamp; word : words) {
freq[word] ;
}
auto end = high_resolution_clock::now();
Добавлено: Существует также стоимость отмены распределения для «freq» (и robin_hood там кажется быстрее). Всего лишь часть построения карты, но все еще значительная.
Ответ №2:
Я сам нашел ответ на этот пост. Python использует рандомизированные алгоритмы хеширования, которые сделают практически невозможным создание двух строк (состоящих из полностью строчных или полностью прописных символов), которые генерируют одинаковые значения хэша.
Теперь, переходя к проблемам производительности C по сравнению с Python3, я также учитывал время, затрачиваемое на чтение и запись, из-за чего C показывался медленнее, чем Python3.
Когда я исправил коды и убедился, что при чтении/записи или назначении нет дополнительных накладных расходов (в случае C я использовал const stringamp;
ссылки на записи карты), я обнаружил, что C работает быстрее, чем Python3, когда gp_hash_table
используется.
Спасибо всем, кто потратил время/усилия, работая над моим вопросом.
Ответ №3:
Возможно, это конструкция копирования в этом цикле for:
for(string word : words) {
freq[word] ;
}
«Строковое слово» копирует новую строку каждую итерацию. Рассмотрите возможность перехода на доступ по ссылке, как это:
for(const stringamp; word : words) {
freq[word] ;
}