19 lut 2011

Z cyklu tajemnice tego świata: karty SD

Udało mi się dzisiaj dowiedzieć, czemu karty pamięci Kingstona są tak dobrze wspierane. Otóż producent udostępnia ich specyfikację na swojej stronie za darmo wbrew oficjalnej organizacji (zwanej szumnie SD Association) pobierającej za świadczenie uprzejmości dostępu do dokumentacji drobne 1000 USD rocznie.

18 sie 2010

Niech ich szlag!

Znowu o muzyce, a raczej o jej braku. Wchodząc sobie na filmik (raczej slajdowisko z muzyką) na YouTube, umieszczony pod adresem: YouTube otrzymałem informację:

"Ten film wideo zawiera treść od partnera Sony Music Entertainment, który zablokował go w Twoim kraju na mocy praw autorskich."

Teraz puenta: powyższy link prowadzi do nagrania Nokturnu Es-dur op.9 cz. 2 autorstwa Polaka - Fryderyka Chopina, wykonywanego przez innego Polaka (pochodzenia żydowskiego) - Artura Rubinsteina, a ja siedzę sobie przed komputerem w Polsce i czytam, że banda Japończyków mi odcięła dostęp do tej muzyki.

24 lip 2010

Wydajność Django

Najwyższa pora zastanowić się, jak to jest z tą wydajnością w Django. Dla przykładu stworzymy sobie prostą aplikację, która będzie miała za zadanie jedynie generować tymczasowe dane. Dla ułatwienia generujemy tylko i wyłącznie prosty kod html, bez korzystania z szablonów. Naszym celem będzie sprawdzenie wydajności samego Pythona w charakterze szybkości obsługi żądania, wpływ zapytań do bazy danych na wydajność, a wreszcie - przyspieszenie dzięki użyciu memcache. Testy przeprowadzimy na serwerze www wbudowanym do Django. Będziemy mierzyli parametry pracy Django dla 1000 zapytań.

Zadanie 1: widok zwracający statyczny html. Kod funkcji:

def test(request):
return HttpResponse('<html>Hello!</html>')


Uruchamiamy serwer oraz testujemy:

Total transferred: 152000 bytes
HTML transferred: 19000 bytes
Requests per second: 961.41 [#/sec] (mean)
Time per request: 1.040 [ms] (mean)
Time per request: 1.040 [ms] (mean, across all concurrent requests)
Transfer rate: 142.71 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 1 1 0.8 1 22
Waiting: 0 1 0.8 1 21
Total: 1 1 0.8 1 22

Wydajność rzędu ok. 960 req/s jest jak najbardziej dobra. Dla porównania, serwer bochnianin.pl nie wytrzymała najazdu 60000 wejść na dobę (przy założeniu, że ruch koncentruje się max. 8h w ciągu doby, daje to zaledwie ~3 req/s). Jesteśmy zatem 300x do przodu.

Zadanie 2: Widok odwołujący się do bazy danych (jedno zapytanie). Kod funkcji:

def test(request):
wal = Waluta.objects.get(pk='USD')
return HttpResponse('<html>Hello! - %s</html>' % wal.nazwa)


Total transferred: 173000 bytes
HTML transferred: 40000 bytes
Requests per second: 130.48 [#/sec] (mean)
Time per request: 7.664 [ms] (mean)
Time per request: 7.664 [ms] (mean, across all concurrent requests)
Transfer rate: 22.04 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 1
Processing: 6 8 1.4 7 19
Waiting: 4 7 1.4 7 18
Total: 6 8 1.4 7 19

Ilość obsłużonych żądań na sekundę spadła z magicznych okolic 960 req/s do ok. 130 req/s, co jest jednak nadal bardzo dobrym rezultatem. Pamiętajmy jednak, że jest to wynik dla zaledwie jednego prostego zapytania. Teraz będzie ciut trudniej - w kolejnym przykładzie zapytań będzie więcej, a także dojdzie łączenie relacją typu wiele-do-wielu. Zobaczmy, jak sprawdzi się Django.

Zadanie 3: Widok pobierający z bazy danych więcej informacji, łączenie wiele-do-wielu. Kod funkcji:

def test(request):
wal = Waluta.objects.get(pk='USD')
wartosc = WartoscWaluty.objects.get(waluta=wal)
kurs = wartosc.kurs
return HttpResponse('<html>Hello! - %s</html>' % kurs)

Wyniki:

Total transferred: 182000 bytes
HTML transferred: 49000 bytes
Requests per second: 91.41 [#/sec] (mean)
Time per request: 10.940 [ms] (mean)
Time per request: 10.940 [ms] (mean, across all concurrent requests)
Transfer rate: 16.25 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 9 11 1.4 11 25
Waiting: 8 10 1.3 10 24
Total: 9 11 1.4 11 25


Zwiększenie ilości zapytań do bazy danych spowolniło działanie aplikacji o ok. 40 reg/s. Czy to dużo? Dla porównania - standardowo Wordpress generuje ok. 6 req/s. Pamiętajmy jednak, że w naszym przypadku serwujemy bardzo mało danych, a sama aplikacja jest niezwykle prymitywna, nieporównywalna rozmiarowo z Wordpressem. W podanym przeze mnie linku, autor chwali się, że dzięki użyciu mechanizmu cache, udało mu się przyspieszyć aplikację do ok. 395 req/s. Spróbujmy i my. Użyjemy Memcache.

Zadanie 4: Dodajemy cache. Kod funkcji:

@cache_page(60*15)
def test(request):
wal = Waluta.objects.get(pk='USD')
wartosc = WartoscWaluty.objects.get(waluta=wal)
kurs = wartosc.kurs
return HttpResponse('<html>Hello! - %s</html>' % kurs)

Cache ustawiony jest na 15 minut. Test:

Total transferred: 338000 bytes
HTML transferred: 49000 bytes
Requests per second: 605.64 [#/sec] (mean)
Time per request: 1.651 [ms] (mean)
Time per request: 1.651 [ms] (mean, across all concurrent requests)
Transfer rate: 199.91 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 1 2 1.2 1 33
Waiting: 1 1 1.2 1 32
Total: 1 2 1.2 1 33

Wróciliśmy do trzycyfrowego wyniku, osiągając wynik ponad 605 req/s! To ponad 200 req/s szybciej niż najbardziej przyspieszony Wordpress. Kluczowe pytanie: czy my tak naprawdę potrzebujemy aż tak szybkiego softu? Policzmy, zakładając, że serwujemy 8 godzin na dobę naszą stronę z maksymalną szybkością 605 req/s, to w ciągu miesiąca (30 dni) mamy: 522720000 (słownie: pół miliarda) wejść na miesiąc! Sądzę, że taką wydajność muszą mieć serwisy kalibru YouTube. Jeśli zaś nie użyjemy mechanizmu cache i spróbujemy działać przy szybkości 80 req/s, to okaże się, że nadal możemy obsługiwać ilość wejść na poziomie 69120000 (czyli ponad 69 milionów odsłon), co nadal jest doskonałym wynikiem.

Konkluzja: dla szybkości działania serwisu, oprócz sprawności samego oprogramowania generującego kod html ważna jest także przepustowość łącz, efektywność baz danych, siedzących "pod spodem" oraz specyfika systemu operacyjnego, na którym to wszystko działa. Mój mały test pokazuje tylko zmiany wydajności frameworka Django w zależności od ilości zapytań do bazy danych oraz zastosowanych optymalizacji. W zestawieniu [jak dla mnie] brakuje tylko jednego elementu: testu wydajności Django, uruchomionego na interpreterze pochodzącym z projektu unladen-swallow. Wtedy też otrzymalibyśmy miarodajne przełożenie w postaci wzrostu [lub spadku ;)] ilości obsłużonych żądań na sekundę.

4 lip 2010

Python - problemy

Tym razem kilka słów o niedoskonałościach Pythona, które powodują, że zostaje on nieco w tyle za chociażby Javą. Zacznijmy od tego, że Java dysponuje bardzo zaawansowaną maszyną wirtualną. Umożliwia ona współbieżne wykonywanie kilku wątków. Dla przykładu zademonstruję program, który zgodnie z wszelaką logiką powinien wykonywać się współbieżnie. Zakładamy, że oba wątki nie korzystają ze wspólnych danych i nie potrzebujemy tutaj żadnych blokad, które mogłyby być przeszkodą do wykonywania współbieżnego.


#!/usr/bin/env python

from threading import Thread

class moj_watek(Thread):
def __init__(self):
Thread.__init__(self)

def run(self):
a = 1
while(1): a+=a/2

watek = moj_watek()
watek.start()
b = 1
while 1: b+=b/2


Po uruchomieniu programu chcielibyśmy, aby nasz kod był równolegle wykonywany na dwóch procesorach (tyle mam w laptopie). Przypatrzmy się zatem, co się dzieje w komputerze po uruchomieniu programu:



Widzimy, że procesory są używane tylko w ok. 50%, chociaż dwa wątki powinny zajmować cały czas obu procesorów. Dlaczego? Python ma w sobie wbudowaną blokadę - tzw. Global Interpreter Lock (GIL). Dobry opis tego problemu w języku polskim można znaleźć tutaj: "Python, wątki i GIL. Fakty i mity."
Problem jest o tyle poważny, że obecnie współbieżność programów w Pythonie uzyskuje się tworząc wiele procesów. Oczywiście ten sposób jest bardzo uciążliwy (powolność IPC, problem z przekazywaniem otwartych deskryptorów gniazd i plików etc.). Jednym z celów wersji 3.2 jest zmodyfikowanie GIL-a tak, aby zamiast zliczania instrukcji kodu bajtowego Pythona opierała się na pomiarze czasu, co pozwoliłoby uniknąć problemów opisanych tutaj: Inside the Python GIL.

Z drugiej strony mamy jeszcze inne implementacje Pythona niż standardowy CPython. Warto tutaj wspomnieć o Jythonie, czyli o implementacji Pythona w Javie. Tutaj wielowątkowość w żaden sposób nie jest problematyczna, a pojęcie globalnej blokady interpretera fizycznie nie istnieje.

Jeszcze jedna sprawa. Python po kilkunastu latach istnienia doczekał się w końcu własnego JIT-a, bazującego na LLVM. W wersji 3.3 wysiłki projektu Unladen Swallow zostaną zintegrowane z oryginalnym CPythonem. Dokładny opis zamieszczony jest tutaj: integracja Unladen Swallow. Co prawda nie osiągnięto założonego, pięciokrotnego przyspieszenia, jednak sami autorzy przyznają, że potencjał technologii JIT jest tak duży, że nawet nie zbliżyli się do jej pełnego wykorzystania. Miejmy więc nadzieję, że wysiłek pracowników Google, autorów LLVM oraz twórców Pythona pozwoli w końcu mocno przyspieszyć ten jakże efektywny dla programisty język.

4 cze 2010

Profiler dla Pythona

Dzisiaj nieco tekstu technicznego. W swojej pracy miałem styczność z jboss-profilerem, czyli programem napisanym w Javie, a umożliwiającym podgląd efektywności napisanego kodu. Profiler ten, oprócz standardowych informacji o czasie wykonywania poszczególnych funkcji udostępniał dane o ilości alokacji poszczególnych obiektów. W Pythonie mamy co prawda cProfiler, który także generuje dane o czasach wykonania poszczególnych funkcji, jednak nie udostępnia on informacji o ilości instancji danego obiektu oraz rozmiarze pamięci zajmowanej przez obiekt. Oto moje rozwiązanie, czyli profiler monitorujący za pomocą modułu Garbage Collectora liczbę zaalokowanych i nie zwolnionych obiektów w pamięci:


import sys
import os
import gc
import types
import inspect
import atexit

def check_allocations():
dt = {}
#gc.collect() jeśli byśmy chcieli info tylko o obiektach żyjących
for obj in gc.get_objects():
try:
if dt.has_key(obj.__class__.__name__):
instances,number,size,typ,bi,module = dt[obj.__class__.__name__]
number+=1
size+=sys.getsizeof(obj)
if obj not in instances:
instances.append(obj)
dt[obj.__class__.__name__] = (instances,number,size,typ,bi,module)
else:
dt[obj.__class__.__name__] = ([obj],1,sys.getsizeof(obj),type(obj),inspect.isbuiltin(obj),obj.__module__)
except AttributeError as e:
pass
return dt

def run():
data = check_allocations()
print '\n\nCurrent allocations'
print '\t%40s | %9s | %9s bytes | <<%s>>' % ('Class name', 'number', 'size', 'module')
print '\t-----------------------------------------------------------------------------------'
for key in data:
instcs,number,size,typ,bi,module = data[key]
if module == '__main__':
print '\t%-40.40s | %9d | %9d bytes | <<%s>>' % (key, number, size, module)
for inst in instcs:
print '\t -> %14.14s 0x%016x | %9s | %9s bytes | <<%s>>' % ('id', id(inst),' ',sys.getsizeof(inst),'---')

print gc.garbage
print '\n\n'

def main():
import os, sys
from optparse import OptionParser
usage = "profiler.py scriptfile"
parser = OptionParser(usage=usage)
parser.allow_interspersed_args = False

if not sys.argv[1:]:
parser.print_usage()
sys.exit(2)
(options,args) = parser.parse_args()

sys.argv[:] = args

if len(sys.argv) > 0:
import __main__
dict = __main__.__dict__

sys.path.insert(0, os.path.dirname(sys.argv[0]))
gc.set_threshold(0)
gc.set_debug(gc.DEBUG_SAVEALL)
try:
exec 'execfile(%r)' % sys.argv[0] in dict, dict
except KeyboardInterrupt as ki:
pass
atexit.register(run)
else:
parser.print_usage()

if __name__ == '__main__':
main()



Kompletny kod jest dostępny tutaj.

Powyższy kod ma obecnie tylko i wyłącznie wartość demonstracyjną, może służyć do badania małych skryptów. Jeśli chcielibyśmy podglądać większe programy, warto w funkcji run ustawić filtrowanie modułów na te, które chcemy monitorować.

Korzystanie z mojego profilera jest analogiczne do tego z cProfilera:


python -m profiler monitorowany_skrypt.py


Miłego korzystania ;)