Ulepsz pandy (PyTables?) Wydajność zapisu tabeli HDF5
Już od dwóch miesięcy korzystam z pand do badań, aby uzyskać wspaniały efekt. Dzięki dużej liczbie zestawów danych zdarzeń śledzenia średniej wielkości pandy + PyTables (interfejs HDF5) mają ogromne zadanie, pozwalając mi przetwarzać heterogeniczne dane przy użyciu wszystkich znanych mi narzędzi Pythona.
Ogólnie rzecz biorąc, używam formatu Fixed (dawniej „Storer”) w PyTables, ponieważ mój przepływ pracy jest jednokrotny do zapisu, wiele do odczytu, a wiele moich zbiorów danych ma takie rozmiary, że mogę załadować 50-100 z nich do pamięci czas bez poważnych wad. (Uwaga: Wiele pracy wykonuję na komputerach klasy Opteron z pamięcią 128 GB +).
Jednak w przypadku dużych zbiorów danych (500 MB i więcej) chciałbym móc korzystać z bardziej skalowalnych możliwości dostępu losowego i zapytań w formacie „Tabele” PyTables, dzięki czemu mogę wykonywać moje zapytania poza pamięcią, a następnie załaduj znacznie mniejszy zestaw wyników do pamięci w celu przetworzenia. Dużą przeszkodą jest jednak wydajność zapisu. Tak, jak powiedziałem, mój przepływ pracy jest jednokrotny, odczytany, ale względne czasy są wciąż niedopuszczalne.
Jako przykład, niedawno uruchomiłem dużą faktoryzację Choleskiego, która zajęła 3 minuty, 8 sekund (188 sekund) na moim 48-rdzeniowym komputerze. To wygenerowało plik śledzenia o ~ 2,2 GB - ślad jest generowany równolegle z programem, więc nie ma dodatkowego „czasu tworzenia śledzenia”.
Początkowa konwersja mojego pliku śledzenia binarnego do formatu pandas / PyTables zajmuje przyzwoity czas, ale w dużej mierze dlatego, że format binarny jest celowo nieczynny w celu zmniejszenia wpływu samego generatora śledzenia na wydajność. Nie ma to również znaczenia dla utraty wydajności podczas przechodzenia z formatu Storer do formatu Table.
Moje testy były początkowo uruchamiane z pandami 0.12, numpy 1.7.1, PyTables 2.4.0 i numexpr 0.20.1. Moja 48-rdzeniowa maszyna działa z częstotliwością 2,8 GHz na rdzeń i piszę do systemu plików ext3, który prawdopodobnie (ale nie na pewno) jest na dysku SSD.
Mogę napisać cały zbiór danych do pliku HDF5 w formacie Storer (w wyniku tego rozmiar pliku: 3,3 GB) w 7,1 sekundy. Ten sam zestaw danych zapisany w formacie tabeli (wynikowy rozmiar pliku to także 3,3 GB) zajmuje 178,7 sekundy.
Kod jest następujący:
with Timer() as t:
store = pd.HDFStore('test_storer.h5', 'w')
store.put('events', events_dataset, table=False, append=False)
print('Fixed format write took ' + str(t.interval))
with Timer() as t:
store = pd.HDFStore('test_table.h5', 'w')
store.put('events', events_dataset, table=True, append=False)
print('Table format write took ' + str(t.interval))
a wyjście jest proste
Fixed format write took 7.1
Table format write took 178.7
Mój zestaw danych ma 28 880 943 wiersze, a kolumny to podstawowe typy danych:
node_id int64
thread_id int64
handle_id int64
type int64
begin int64
end int64
duration int64
flags int64
unique_id int64
id int64
DSTL_LS_FULL float64
L2_DMISS float64
L3_MISS float64
kernel_type float64
dtype: object
... więc nie sądzę, aby z szybkością zapisu były jakieś problemy związane z danymi.
Próbowałem też dodać kompresję BLOSC, aby wykluczyć wszelkie dziwne problemy we / wy, które mogą mieć wpływ na jeden lub drugi scenariusz, ale kompresja wydaje się zmniejszać wydajność obu jednakowo.
Teraz zdaję sobie sprawę, że dokumentacja pand mówi, że format Storer oferuje znacznie szybsze zapisy i nieco szybsze odczyty. (Doświadczam szybszych odczytów, ponieważ odczyt formatu Storer wydaje się trwać około 2,5 sekundy, podczas gdy odczyt formatu tabeli zajmuje około 10 sekund.) Ale naprawdę wydaje się, że zapis w formacie tabeli powinien zająć 25 razy więcej niż tak długo, jak zapisuje się format Storer.
Czy którakolwiek z osób zaangażowanych w PyTables lub pandy może wyjaśnić powody architektoniczne (lub inne) powodujące, że pisanie do formatu zapytywalnego (który wymaga bardzo mało dodatkowych danych) powinno trwać o rząd wielkości dłużej? Czy jest jakaś nadzieja na poprawę tego w przyszłości? Chciałbym wskoczyć do jednego lub drugiego projektu, ponieważ moja dziedzina to wysokowydajne obliczenia i widzę znaczący przypadek użycia w obu projektach w tej domenie .... ale byłoby pomocne wyjaśnienie tego problemy dotyczyły najpierw i / lub kilka porad, jak przyspieszyć proces od tych, którzy wiedzą, jak zbudowany jest system.
EDYTOWAĆ:
Uruchomienie poprzednich testów z% prun w IPython daje następujące (nieco zredukowane dla czytelności) dane wyjściowe profilu dla formatu Storer / Fixed:
%prun -l 20 profile.events.to_hdf('test.h5', 'events', table=False, append=False)
3223 function calls (3222 primitive calls) in 7.385 seconds
Ordered by: internal time
List reduced from 208 to 20 due to restriction <20>
ncalls tottime percall cumtime percall filename:lineno(function)
6 7.127 1.188 7.128 1.188 {method '_createArray' of 'tables.hdf5Extension.Array' objects}
1 0.242 0.242 0.242 0.242 {method '_closeFile' of 'tables.hdf5Extension.File' objects}
1 0.003 0.003 0.003 0.003 {method '_g_new' of 'tables.hdf5Extension.File' objects}
46 0.001 0.000 0.001 0.000 {method 'reduce' of 'numpy.ufunc' objects}
i następujące dla formatu tabel:
%prun -l 40 profile.events.to_hdf('test.h5', 'events', table=True, append=False, chunksize=1000000)
499082 function calls (499040 primitive calls) in 188.981 seconds
Ordered by: internal time
List reduced from 526 to 40 due to restriction <40>
ncalls tottime percall cumtime percall filename:lineno(function)
29 92.018 3.173 92.018 3.173 {pandas.lib.create_hdf_rows_2d}
640 20.987 0.033 20.987 0.033 {method '_append' of 'tables.hdf5Extension.Array' objects}
29 19.256 0.664 19.256 0.664 {method '_append_records' of 'tables.tableExtension.Table' objects}
406 19.182 0.047 19.182 0.047 {method '_g_writeSlice' of 'tables.hdf5Extension.Array' objects}
14244 10.646 0.001 10.646 0.001 {method '_g_readSlice' of 'tables.hdf5Extension.Array' objects}
472 10.359 0.022 10.359 0.022 {method 'copy' of 'numpy.ndarray' objects}
80 3.409 0.043 3.409 0.043 {tables.indexesExtension.keysort}
2 3.023 1.512 3.023 1.512 common.py:134(_isnull_ndarraylike)
41 2.489 0.061 2.533 0.062 {method '_fillCol' of 'tables.tableExtension.Row' objects}
87 2.401 0.028 2.401 0.028 {method 'astype' of 'numpy.ndarray' objects}
30 1.880 0.063 1.880 0.063 {method '_g_flush' of 'tables.hdf5Extension.Leaf' objects}
282 0.824 0.003 0.824 0.003 {method 'reduce' of 'numpy.ufunc' objects}
41 0.537 0.013 0.668 0.016 index.py:607(final_idx32)
14490 0.385 0.000 0.712 0.000 array.py:342(_interpret_indexing)
39 0.279 0.007 19.635 0.503 index.py:1219(reorder_slice)
2 0.256 0.128 10.063 5.031 index.py:1099(get_neworder)
1 0.090 0.090 119.392 119.392 pytables.py:3016(write_data)
57842 0.087 0.000 0.087 0.000 {numpy.core.multiarray.empty}
28570 0.062 0.000 0.107 0.000 utils.py:42(is_idx)
14164 0.062 0.000 7.181 0.001 array.py:711(_readSlice)
EDYCJA 2:
Uruchamiając ponownie z przedpremierową kopią pand 0.13 (wyciągniętą 20 listopada 2013 r. Około 11:00 EST), czasy zapisu dla formatu tabel poprawiają się znacznie, ale nadal nie porównują „rozsądnie” z prędkościami zapisu Storer / Fixed format.
%prun -l 40 profile.events.to_hdf('test.h5', 'events', table=True, append=False, chunksize=1000000)
499748 function calls (499720 primitive calls) in 117.187 seconds
Ordered by: internal time
List reduced from 539 to 20 due to restriction <20>
ncalls tottime percall cumtime percall filename:lineno(function)
640 22.010 0.034 22.010 0.034 {method '_append' of 'tables.hdf5Extension.Array' objects}
29 20.782 0.717 20.782 0.717 {method '_append_records' of 'tables.tableExtension.Table' objects}
406 19.248 0.047 19.248 0.047 {method '_g_writeSlice' of 'tables.hdf5Extension.Array' objects}
14244 10.685 0.001 10.685 0.001 {method '_g_readSlice' of 'tables.hdf5Extension.Array' objects}
472 10.439 0.022 10.439 0.022 {method 'copy' of 'numpy.ndarray' objects}
30 7.356 0.245 7.356 0.245 {method '_g_flush' of 'tables.hdf5Extension.Leaf' objects}
29 7.161 0.247 37.609 1.297 pytables.py:3498(write_data_chunk)
2 3.888 1.944 3.888 1.944 common.py:197(_isnull_ndarraylike)
80 3.581 0.045 3.581 0.045 {tables.indexesExtension.keysort}
41 3.248 0.079 3.294 0.080 {method '_fillCol' of 'tables.tableExtension.Row' objects}
34 2.744 0.081 2.744 0.081 {method 'ravel' of 'numpy.ndarray' objects}
115 2.591 0.023 2.591 0.023 {method 'astype' of 'numpy.ndarray' objects}
270 0.875 0.003 0.875 0.003 {method 'reduce' of 'numpy.ufunc' objects}
41 0.560 0.014 0.732 0.018 index.py:607(final_idx32)
14490 0.387 0.000 0.712 0.000 array.py:342(_interpret_indexing)
39 0.303 0.008 19.617 0.503 index.py:1219(reorder_slice)
2 0.288 0.144 10.299 5.149 index.py:1099(get_neworder)
57871 0.087 0.000 0.087 0.000 {numpy.core.multiarray.empty}
1 0.084 0.084 45.266 45.266 pytables.py:3424(write_data)
1 0.080 0.080 55.542 55.542 pytables.py:3385(write)
Zauważyłem podczas uruchamiania tych testów, że istnieją długie okresy, w których pisanie wydaje się „wstrzymywać” (plik na dysku nie jest aktywnie rozwijany), a mimo to w niektórych z tych okresów jest również niskie użycie procesora.
Zaczynam podejrzewać, że niektóre znane ograniczenia ext3 mogą źle współdziałać z pandami lub PyTables. Ext3 i inne systemy plików nieoparte na rozmiarze czasami mają trudności z natychmiastowym odłączeniem dużych plików, a podobna wydajność systemu (niskie użycie procesora, ale długie czasy oczekiwania) jest widoczna nawet na przykład w przypadku zwykłego „rm” pliku 1 GB.
Aby wyjaśnić, w każdym przypadku testowym, upewniłem się, że usunąłem istniejący plik, jeśli taki istnieje, przed rozpoczęciem testu, aby nie ponieść żadnej kary za usunięcie / nadpisanie pliku ext3.
Jednak po ponownym uruchomieniu tego testu z indeksem = Brak, wydajność poprawia się drastycznie (~ 50s vs ~ 120 podczas indeksowania). Wydaje się więc, że albo ten proces jest nadal związany z procesorem (mój system ma stosunkowo stare procesory AMD Opteron Istanbul działające przy 2,8 GHz, ale ma także 8 gniazd z 6 rdzeniowymi procesorami w każdym, z których wszystkie oprócz jednego z oczywiście, siedzieć bezczynnie podczas zapisu), lub że istnieje jakiś konflikt między sposobem, w jaki PyTables lub pand próbuje manipulować / czytać / analizować plik, gdy jest już częściowo lub w całości na systemie plików, który powoduje patologicznie złe zachowanie we / wy, gdy indeksowanie jest występujący.
EDYCJA 3:
Sugerowane testy @ Jeffa na mniejszym zbiorze danych (1,3 GB na dysku), po uaktualnieniu PyTables z 2.4 do 3.0.0, dostałem tutaj:
In [7]: %timeit f(df)
1 loops, best of 3: 3.7 s per loop
In [8]: %timeit f2(df) # where chunksize= 2 000 000
1 loops, best of 3: 13.8 s per loop
In [9]: %timeit f3(df) # where chunksize= 2 000 000
1 loops, best of 3: 43.4 s per loop
W rzeczywistości moja wydajność wydaje się bić go we wszystkich scenariuszach, z wyjątkiem sytuacji, gdy indeksowanie jest włączone (ustawienie domyślne). Jednak indeksowanie nadal wydaje się być zabójcą i jeśli sposób, w jaki interpretuję dane wyjściowetop
ils
podczas wykonywania tych testów jest poprawne, pozostają okresy czasu, w których nie ma ani znaczącego przetwarzania, ani żadnego zapisu pliku (tj. użycie procesora w procesie Pythona jest bliskie 0, a rozmiar pliku pozostaje stały). Mogę tylko założyć, że są to odczyty plików. Trudno mi zrozumieć, dlaczego odczytywanie plików powodowałoby spowolnienie, ponieważ mogę niezawodnie załadować cały plik 3+ GB z tego dysku do pamięci w czasie poniżej 3 sekund. Jeśli nie są to odczyty plików, to na czym polega „oczekiwanie” systemu? (Nikt inny nie jest zalogowany do komputera i nie ma innej aktywności systemu plików.)
W tym momencie, dzięki ulepszonym wersjom odpowiednich modułów Pythona, wydajność mojego oryginalnego zestawu danych jest niższa niż poniższe liczby. Szczególnie interesujący jest czas systemowy, który, jak przypuszczam, jest przynajmniej górnym limitem czasu spędzanego na wykonywaniu IO i czasu Wall, co wydaje się być prawdopodobnie przyczyną tych tajemniczych okresów braku aktywności zapisu / braku procesora.
In [28]: %time f(profile.events)
CPU times: user 0 ns, sys: 7.16 s, total: 7.16 s
Wall time: 7.51 s
In [29]: %time f2(profile.events)
CPU times: user 18.7 s, sys: 14 s, total: 32.7 s
Wall time: 47.2 s
In [31]: %time f3(profile.events)
CPU times: user 1min 18s, sys: 14.4 s, total: 1min 32s
Wall time: 2min 5s
Niemniej jednak wydaje się, że indeksowanie powoduje znaczne spowolnienie mojego przypadku użycia. Być może powinienem spróbować ograniczyć indeksowane pola zamiast po prostu wykonywać domyślny przypadek (który może bardzo dobrze być indeksowany na wszystkich polach DataFrame)? Nie jestem pewien, w jaki sposób może to wpłynąć na czasy zapytań, szczególnie w przypadkach, w których zapytanie wybierane jest na podstawie pola niezindeksowanego.
Na prośbę Jeffa ptdump pliku wynikowego.
ptdump -av test.h5
/ (RootGroup) ''
/._v_attrs (AttributeSet), 4 attributes:
[CLASS := 'GROUP',
PYTABLES_FORMAT_VERSION := '2.1',
TITLE := '',
VERSION := '1.0']
/df (Group) ''
/df._v_attrs (AttributeSet), 14 attributes:
[CLASS := 'GROUP',
TITLE := '',
VERSION := '1.0',
data_columns := [],
encoding := None,
index_cols := [(0, 'index')],
info := {1: {'type': 'Index', 'names': [None]}, 'index': {}},
levels := 1,
nan_rep := 'nan',
non_index_axes :=
[(1, ['node_id', 'thread_id', 'handle_id', 'type', 'begin', 'end', 'duration', 'flags', 'unique_id', 'id', 'DSTL_LS_FULL', 'L2_DMISS', 'L3_MISS', 'kernel_type'])],
pandas_type := 'frame_table',
pandas_version := '0.10.1',
table_type := 'appendable_frame',
values_cols := ['values_block_0', 'values_block_1']]
/df/table (Table(28880943,)) ''
description := {
"index": Int64Col(shape=(), dflt=0, pos=0),
"values_block_0": Int64Col(shape=(10,), dflt=0, pos=1),
"values_block_1": Float64Col(shape=(4,), dflt=0.0, pos=2)}
byteorder := 'little'
chunkshape := (4369,)
autoindex := True
colindexes := {
"index": Index(6, medium, shuffle, zlib(1)).is_csi=False}
/df/table._v_attrs (AttributeSet), 15 attributes:
[CLASS := 'TABLE',
FIELD_0_FILL := 0,
FIELD_0_NAME := 'index',
FIELD_1_FILL := 0,
FIELD_1_NAME := 'values_block_0',
FIELD_2_FILL := 0.0,
FIELD_2_NAME := 'values_block_1',
NROWS := 28880943,
TITLE := '',
VERSION := '2.7',
index_kind := 'integer',
values_block_0_dtype := 'int64',
values_block_0_kind := ['node_id', 'thread_id', 'handle_id', 'type', 'begin', 'end', 'duration', 'flags', 'unique_id', 'id'],
values_block_1_dtype := 'float64',
values_block_1_kind := ['DSTL_LS_FULL', 'L2_DMISS', 'L3_MISS', 'kernel_type']]
i kolejny% prun ze zaktualizowanymi modułami i pełnym zestawem danych:
%prun -l 25 %time f3(profile.events)
CPU times: user 1min 14s, sys: 16.2 s, total: 1min 30s
Wall time: 1min 48s
542678 function calls (542650 primitive calls) in 108.678 seconds
Ordered by: internal time
List reduced from 629 to 25 due to restriction <25>
ncalls tottime percall cumtime percall filename:lineno(function)
640 23.633 0.037 23.633 0.037 {method '_append' of 'tables.hdf5extension.Array' objects}
15 20.852 1.390 20.852 1.390 {method '_append_records' of 'tables.tableextension.Table' objects}
406 19.584 0.048 19.584 0.048 {method '_g_write_slice' of 'tables.hdf5extension.Array' objects}
14244 10.591 0.001 10.591 0.001 {method '_g_read_slice' of 'tables.hdf5extension.Array' objects}
458 9.693 0.021 9.693 0.021 {method 'copy' of 'numpy.ndarray' objects}
15 6.350 0.423 30.989 2.066 pytables.py:3498(write_data_chunk)
80 3.496 0.044 3.496 0.044 {tables.indexesextension.keysort}
41 3.335 0.081 3.376 0.082 {method '_fill_col' of 'tables.tableextension.Row' objects}
20 2.551 0.128 2.551 0.128 {method 'ravel' of 'numpy.ndarray' objects}
101 2.449 0.024 2.449 0.024 {method 'astype' of 'numpy.ndarray' objects}
16 1.789 0.112 1.789 0.112 {method '_g_flush' of 'tables.hdf5extension.Leaf' objects}
2 1.728 0.864 1.728 0.864 common.py:197(_isnull_ndarraylike)
41 0.586 0.014 0.842 0.021 index.py:637(final_idx32)
14490 0.292 0.000 0.616 0.000 array.py:368(_interpret_indexing)
2 0.283 0.142 10.267 5.134 index.py:1158(get_neworder)
274 0.251 0.001 0.251 0.001 {method 'reduce' of 'numpy.ufunc' objects}
39 0.174 0.004 19.373 0.497 index.py:1280(reorder_slice)
57857 0.085 0.000 0.085 0.000 {numpy.core.multiarray.empty}
1 0.083 0.083 35.657 35.657 pytables.py:3424(write_data)
1 0.065 0.065 45.338 45.338 pytables.py:3385(write)
14164 0.065 0.000 7.831 0.001 array.py:615(__getitem__)
28570 0.062 0.000 0.108 0.000 utils.py:47(is_idx)
47 0.055 0.001 0.055 0.001 {numpy.core.multiarray.arange}
28570 0.050 0.000 0.090 0.000 leaf.py:397(_process_range)
87797 0.048 0.000 0.048 0.000 {isinstance}