എന്റെ മുമ്പത്തെ പോസ്റ്റ് (ഈ സൈറ്റിന്റെ തീം പരീക്ഷിക്കാൻ വേണ്ടി ഉണ്ടാക്കിയതാണ്), ടേംസ് വരെയുള്ള വർഗ്ഗങ്ങളുടെ വിപരീതങ്ങളുടെ തുക കണക്കാക്കുന്ന കുറച്ച് കോഡ് സ്നിപ്പറ്റുകൾ നൽകി. ഞാൻ ഈ കോഡ് എന്റെ പ്രിയപ്പെട്ട 4 ഭാഷകളിൽ—പൈത്തൺ, സി, റസ്റ്റ്, ഹാസ്കെൽ—എഴുതി, പക്ഷേ പൈത്തൺ കോഡ് റൺ ചെയ്യുമ്പോൾ അത് നാണംകെടുത്തുന്നത്ര മന്ദഗതിയിലായിരുന്നു. സീക്വൻഷ്യൽ റസ്റ്റിന് ms എടുത്തപ്പോൾ, പൈത്തണിന് 70 സെക്കൻഡ് എടുത്തു! അതിനാൽ, ഈ പോസ്റ്റിൽ, പൈത്തണിന് കുറച്ച് കൂടുതൽ യുക്തിസഹമായ സംഖ്യകൾ നേടാൻ ശ്രമിക്കാൻ പോകുന്നു.
യഥാർത്ഥ പരിഹാരം
def basel(N: int) -> float:
return sum(x**(-2) for x in range(1,N))
ഇതാണ് ഈ ടാസ്ക് ചെയ്യുന്നതിനുള്ള ഏറ്റവും പൈത്തോണിക് മാർഗം. എളുപ്പത്തിൽ വായിക്കാവുന്ന, ഒറ്റ വരി കോഡ് ആണിത്, ഇത് ബിൽറ്റ്-ഇൻ ജനറേറ്ററുകൾ ഉപയോഗിക്കുന്നു. നമുക്ക് ഇത് സമയം നോക്കാം, ഉപയോഗിച്ച്, കഴിഞ്ഞ പോസ്റ്റിലെ പോലെ അല്ല (കാരണം ഞാൻ അത്രയും സമയം കാത്തിരിക്കാൻ ആഗ്രഹിക്കുന്നില്ല):
import time
# ഇൻപുട്ട് ഫംഗ്ഷൻ സമയം നോക്കുക
def time_function(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
execution_time = end_time - start_time
print(f"Function '{func.__name__}' executed in {execution_time:.6f} seconds.")
return result
return wrapper
>>> f = time_function(basel)
>>> f(100000000) # 10^8
Function 'basel' executed in 6.641589 seconds.
1.644934057834575
നമുക്ക് ഇത് വീണ്ടും എഴുതാം, പക്ഷേ കുറച്ച് കുറഞ്ഞ പൈത്തോണിക്, for
ലൂപ്പ് ഉപയോഗിച്ച്:
def basel_less_pythonic(N: int) -> float:
s = 0.0
for x in range(1, N):
s += x**(-2)
return s
>>> f(100000000) # 10^8
Function 'basel_less_pythonic' executed in 5.908466 seconds.
1.644934057834575
രസകരമാണ്. കുറച്ച് കുറഞ്ഞ ഇഡിയോമാറ്റിക് രീതിയിൽ എഴുതിയത് യഥാർത്ഥത്തിൽ പ്രകടനം മെച്ചപ്പെടുത്തി.
എന്തുകൊണ്ട്? നമുക്ക് dis
, പൈത്തൺ ഡിസാസംബ്ലി മൊഡ്യൂൾ ഉപയോഗിച്ച് അന്വേഷിക്കാം.
ഇതാ യഥാർത്ഥ പതിപ്പ്. ഞാൻ ചില സഹായകരമായ കമന്റുകൾ ചേർത്തിട്ടുണ്ട്:
# വേരിയബിളുകൾ ലോഡ് ചെയ്യുക
00 LOAD_GLOBAL 0 (sum)
02 LOAD_CONST 1 (<code object <genexpr> at 0x104f42e40>)
04 LOAD_CONST 2 ('basel.<locals>.<genexpr>')
# ഫംഗ്ഷൻ സൃഷ്ടിക്കുക
06 MAKE_FUNCTION 0
# `range` ഒബ്ജക്റ്റ് സൃഷ്ടിക്കുക
08 LOAD_GLOBAL 1 (range)
10 LOAD_CONST 3 (1)
12 LOAD_FAST 0 (N)
14 CALL_FUNCTION 2
# ഇറ്ററേറ്ററായി മാറ്റുക
16 GET_ITER
# ജനറേറ്റർ ക്ഷണിക്കുക
18 CALL_FUNCTION 1
# സം ഫംഗ്ഷൻ ക്ഷണിക്കുക
20 CALL_FUNCTION 1
# റിട്ടേൺ
22 RETURN_VALUE
f <code object <genexpr> at 0x104f42e40>:
# അടിസ്ഥാനപരമായി `for` ലൂപ്പ് ഉപയോഗിച്ച് yield
00 GEN_START 0
02 LOAD_FAST 0 (.0)
04 FOR_ITER 7 (to 20)
06 STORE_FAST 1 (x)
08 LOAD_FAST 1 (x)
10 LOAD_CONST 0 (-2)
12 BINARY_POWER
14 YIELD_VALUE
16 POP_TOP
18 JUMP_ABSOLUTE 2 (to 4)
20 LOAD_CONST 1 (None)
22 RETURN_VALUE
ഇതാ for
ലൂപ്പ് പതിപ്പ്:
# വേരിയബിളുകൾ ലോഡ് ചെയ്യുക
00 LOAD_CONST 1 (0.0)
02 STORE_FAST 1 (s)
# `range` ഒബ്ജക്റ്റ് സൃഷ്ടിക്കുക
04 LOAD_GLOBAL 0 (range)
06 LOAD_CONST 2 (1)
08 LOAD_FAST 0 (N)
10 CALL_FUNCTION 2
12 GET_ITER
# `for` ലൂപ്പ് ഡിക്ലയർ ചെയ്യുക
14 FOR_ITER 8 (to 32)
16 STORE_FAST 2 (x)
# പവർ, ചേർക്കുക, സ്റ്റോർ ചെയ്യുക
18 LOAD_FAST 1 (s)
20 LOAD_FAST 2 (x)
22 LOAD_CONST 3 (-2)
24 BINARY_POWER
26 INPLACE_ADD
28 STORE_FAST 1 (s)
# `for` ലൂപ്പിന്റെ മുകളിലേക്ക് പോകുക
30 JUMP_ABSOLUTE 7 (to 14)
# `s` റിട്ടേൺ ചെയ്യുക
32 LOAD_FAST 1 (s)
34 RETURN_VALUE
ഈ പ്രത്യേക കേസിൽ, ജനറേറ്റർ ഉപയോഗിച്ചത് പ്രോഗ്രാമിന്റെ പ്രകടനം മന്ദഗതിയിലാക്കി.
നമുക്ക് കാണാനാകുന്നത് പോലെ, ജനറേറ്റർ ലൂപ്പ് കോഡ് രണ്ടാമത്തെ പതിപ്പിന് സമാനമാണ്.
ആദ്യത്തേത് ജനറേറ്റർ ഒബ്ജക്റ്റ് കൈകാര്യം ചെയ്യുന്നതിന് അധിക ജോലി ചെയ്യുന്നു,
ഇതിന് (മന്ദഗതിയിലുള്ള) CALL_FUNCTION
ഡയറക്ടീവുകൾ ആവശ്യമാണ്. പ്രായോഗികമായി, ജനറേറ്ററുകൾ
സാധാരണയായി വേഗതയേറിയതാണ് അവയുടെ മടിയൻ സ്വഭാവം കാരണം. ഈ കേസിൽ മാത്രം,
അധിക ബ്ലോട്ട് അതിന്റെ മൂല്യം നൽകുന്നില്ല.
എന്നിരുന്നാലും, രണ്ട് പതിപ്പുകൾ തമ്മിലുള്ള പ്രകടന വ്യത്യാസം ( s) പൈത്തൺ, സി/റസ്റ്റ് തമ്മിലുള്ള മൊത്തം വ്യത്യാസത്തോട് താരതമ്യപ്പെടുത്തുമ്പോൾ നിസ്സാരമാണ്. വേഗതയേറിയ പതിപ്പ് ഉപയോഗിച്ചാലും, റസ്റ്റ് കോഡ് പൈത്തണിനേക്കാൾ മടങ്ങ് വേഗതയുള്ളതാണ്.
എന്തുകൊണ്ട്? പ്രധാനമായും പൈത്തൺ ഒരു ലൂസ്-ടൈപ്പ്ഡ്, ഇന്റർപ്രെറ്റഡ് ഭാഷയാണ്. ഇതിനർത്ഥം പൈത്തൺ ഇന്റർപ്രെറ്റർ (CPython), പൈത്തൺ കോഡ് നിങ്ങളുടെ കമ്പ്യൂട്ടർ എളുപ്പത്തിൽ എക്സിക്യൂട്ട് ചെയ്യാൻ കഴിയുന്ന ഒന്നാക്കി മാറ്റണം, റിയൽ-ടൈമിൽ. ഇത് പൈത്തൺ കോഡ് പൈത്തൺ ബൈറ്റ്കോഡ് എന്ന് വിളിക്കപ്പെടുന്ന ഒരു സാധാരണ ഫോമിലേക്ക് വേഗത്തിൽ കംപൈൽ ചെയ്യുന്നതിലൂടെ ചെയ്യുന്നു, അത് നമ്മൾ ഇപ്പോൾ നോക്കി.
ഇത് തുടർന്ന് ഇന്റർപ്രെറ്റർ എക്സിക്യൂട്ട് ചെയ്യുന്നു. ഈ കംപൈലേഷൻ ഘട്ടം പൈത്തണിന് ഉണ്ടാകുന്ന ആദ്യത്തെ പ്രകടന ഹിറ്റാണ്. റസ്റ്റ് പോലുള്ള ഭാഷകൾ ഒരു പ്രോഗ്രാം ഒരിക്കൽ മാത്രം കംപൈൽ ചെയ്യേണ്ടതുണ്ട്, അതിനാൽ ആ സമയം പ്രോഗ്രാമിന്റെ റൺടൈമിൽ കണക്കാക്കുന്നില്ല. എന്നാൽ പ്രകടനത്തിന് ഏറ്റവും വലിയ ഹിറ്റ് വരുന്നത് മോശം നിലവാരമുള്ള, ആർക്കിടെക്ചർ അഗ്നോസ്റ്റിക് ബൈറ്റ്കോഡ് കാരണമാണ്. ഇത് ഇന്റർപ്രെറ്റഡ് ഭാഷകളുടെ സ്വഭാവമാണ്, കാരണം അവയ്ക്ക് ഉയർന്ന നിലവാരമുള്ള കംപൈലേഷനിൽ ഇത്രയധികം സമയം ചെലവഴിക്കാൻ കഴിയില്ല.
അപ്പോൾ, നിങ്ങൾക്ക് എങ്ങനെ വേഗതയേറിയ പൈത്തൺ എഴുതാം? നിങ്ങൾക്ക് കഴിയില്ല. പക്ഷേ, നിങ്ങൾക്ക് വേഗതയേറിയ സി കോഡിലേക്ക് കോൾ ചെയ്യാം, Numpy പോലുള്ള ഹെവിലി ഒപ്റ്റിമൈസ്ഡ് ലൈബ്രറികൾ വഴി. ഈ ലൈബ്രറികളിൽ പ്രീ-കംപൈൽ ചെയ്ത, വെക്റ്ററൈസ്ഡ് സി ഫംഗ്ഷനുകൾ അടങ്ങിയിട്ടുണ്ട്, അത് നിങ്ങളെ മുഴുവൻ പൈത്തൺ ഇന്റർപ്രെറ്റർ ഒഴിവാക്കാൻ അനുവദിക്കുന്നു.
നമുക്ക് Numpy ഉപയോഗിക്കാം
import numpy as np
def basel_np(N: int) -> float:
# [1, 1, ..., 1]
ones = np.ones(N - 1)
# [1, 2, ..., N]
r = np.arange(1, N)
# [1, 1/2, ..., 1/N]
div = ones / r
# [1, 1/4, ..., 1/N^2]
inv_squares = np.square(div)
# ~ pi^2/6
return float(np.sum(inv_squares))
>>> f(100000000) # 10^8
Function 'basel_np' executed in 0.460317 seconds.
1.6449340568482196
വാവ്, അത്
മടങ്ങ് വേഗത്തിൽ പ്രവർത്തിച്ചു! ഈ സാഹചര്യത്തിൽ ബൈറ്റ്കോഡ് നോക്കുന്നത് വളരെയധികം പറയാൻ പറ്റില്ല, കാരണം അത് numpy-യിലേക്കുള്ള കുറച്ച് CALL_FUNCTION
-കൾ മാത്രമായിരിക്കും, യഥാർത്ഥ പ്രവർത്തനം numpy ചെയ്യുന്നു. ഏത് ലൈൻ ആണ് ഏറ്റവും കൂടുതൽ സമയം എടുക്കുന്നത് എന്ന് നോക്കാം:
def basel_np(N: int) -> tuple[float, list[float]]:
times = []
# ഒരു സിംഗിൾ സ്റ്റെപ്പ് സമയം അളക്കുക
start = time.perf_counter()
ones = np.ones(N - 1)
end = time.perf_counter()
step_time = end - start
times.append(step_time)
# ബാക്കിയുള്ള ടൈമിംഗ് കോഡ് ഒഴിവാക്കി
r = np.arange(1, N)
div = ones / r
square = np.square(div)
ret = np.sum(square)
return ret, times
ഓപ്പറേഷൻ | ഓപ്പറേഷന് എടുത്ത സമയം (ms) | കുമുലേറ്റീവ് സമയം (ms) |
---|---|---|
ഒന്നുകൾ സൃഷ്ടിക്കൽ | 97 | 97 |
റേഞ്ച് സൃഷ്ടിക്കൽ | 79 | 176 |
റേഞ്ച് സ്ക്വയർ ചെയ്യൽ | 98 | 274 |
ഒന്നുകൾ/സ്ക്വയറുകൾ ഹരിക്കൽ | 112 | 387 |
ഫൈനൽ സം | 58 | 444 |
ഈ നമ്പറുകൾ ഒരു മിനിറ്റ് ചിന്തിക്കാം. ഒന്നുകൾ/റേഞ്ച് സൃഷ്ടിക്കൽ ഘട്ടങ്ങൾ കഠിനമായ കമ്പ്യൂട്ടേഷണൽ ജോലി ഉൾക്കൊള്ളുന്നില്ല. എന്നിരുന്നാലും, അവ “കഠിനമായ” ഘട്ടങ്ങളായ സ്ക്വയറിംഗും ഡിവിഷനും പോലെയുള്ളവയ്ക്ക് ഏതാണ്ട് തുല്യമായ സമയം എടുക്കുന്നു. ആശ്ചര്യകരമെന്നു പറയട്ടെ, അന്തിമ അറേയിലെ സം ആണ് ഏറ്റവും വേഗത്തിലുള്ള ഘട്ടം! ഇത് സൂചിപ്പിക്കുന്നത് ഇവിടെ ബോട്ടൽനെക്ക് CPU അല്ല, മെമ്മറി ആക്സസ് ആണ്. മുതൽ വരെയുള്ള പ്രകടനം എങ്ങനെ സ്കെയിൽ ചെയ്യുന്നുവെന്ന് നോക്കാം.
ഈ പ്ലോട്ടിൽ, ഏറ്റവും മുകളിലെ ലൈനിന്റെ അറ്റം മൊത്തം സമയത്തെ പ്രതിനിധീകരിക്കുന്നു, തുടർന്നുള്ള ലൈനുകൾക്കിടയിലുള്ള സ്ഥലം ഓരോ ഘട്ടത്തിന്റെയും സമയത്തെ പ്രതിനിധീകരിക്കുന്നു.
നമുക്ക് കാണാം:
-
മൊത്തം സമയം നോൺ-ലീനിയർ ആയി വർദ്ധിക്കുന്നു, ആൽഗോരിതം ആണെങ്കിലും.
-
ഡിവൈഡ്
ഘട്ടം ഏറ്റവും കൂടുതൽ സമയം എടുക്കുന്നു, , എന്നിവയിൽ കൂർത്ത വർദ്ധനവുകൾ ഉണ്ട്.
ഇത് അർത്ഥമാക്കുന്നത്, മറ്റ് ഓപ്പറേഷനുകളിൽ നിന്ന് വ്യത്യസ്തമായി, np.divide
രണ്ട് വലിയ അറേകൾ ഒരേസമയം ആക്സസ് ചെയ്യുകയും ഫലം ഒരു പുതിയ അറേയിലേക്ക് എഴുതുകയും ചെയ്യേണ്ടതുണ്ട്. ഇതിനർത്ഥം ധാരാളം മെയിൻ മെമ്മറി ആക്സസുകളും, സാധ്യതയുള്ള SSD-യിൽ നിന്നുള്ള റീഡുകളും, അത് വളരെ മന്ദഗതിയിലാണ്.
ബ്രോഡ്കാസ്റ്റിംഗ്
ഇത്തരം പ്രശ്നങ്ങൾക്കായി Numpy-യിൽ ഒരു ഒപ്റ്റിമൈസേഷൻ ഉണ്ട്, അതിനെ ബ്രോഡ്കാസ്റ്റിംഗ് എന്ന് വിളിക്കുന്നു. ഇത് ചെറിയ വലിപ്പമുള്ള വെക്ടറുകളെ വലിയ വലിപ്പങ്ങളിലേക്ക് “വിർച്വലായി പ്രൊജക്ട്” ചെയ്യുന്നു, വെക്ടർ-വെക്ടർ ഓപ്പറേഷനുകൾക്കായി.
def basel_np_broadcast(N) -> float:
ones = 1
r = np.arange(1, N)
div = ones / r
square = np.square(div)
# [1, 1, ..., 1] / [1, 4, ..., N^2] എന്നത് പോലെ
return float(np.sum(square))
ഇത് ഞങ്ങളെ വിലപ്പെട്ട കാഷെ സ്ഥലം ലാഭിക്കാൻ സഹായിക്കുന്നു. നമുക്ക് മുമ്പത്തെ അതേ മെട്രിക്സ് പ്രവർത്തിപ്പിക്കാം. ഉപയോഗിച്ച്:
ഓപ്പറേഷൻ | ഓപ്പറേഷന് എടുത്ത സമയം (ms) | സഞ്ചിത സമയം (ms) |
---|---|---|
ones സൃഷ്ടിക്കൽ | 0.00 | 0 |
റേഞ്ച് സൃഷ്ടിക്കൽ | 68.56 | 70.48 |
റേഞ്ച് സ്ക്വയർ ചെയ്യൽ | 105.14 | 180.74 |
ones/squares ഡിവൈഡ് ചെയ്യൽ | 133.08 | 271.30 |
ഫൈനൽ സം | 71.08 | 310.41 |
ഇനി മുതൽ, ബ്രോഡ്കാസ്റ്റ് ചെയ്ത പതിപ്പിനെ “Numpy പരിഹാരം” എന്ന് വിളിക്കാം.
ഒരു പ്രധാന മെച്ചപ്പെടുത്തൽ ആണെങ്കിലും, ഞങ്ങൾ ഇപ്പോഴും ആ ലാറ്റൻസി സ്പൈക്കുകൾ കാണുന്നു. നമുക്ക് അത് പരിഹരിക്കാൻ ശ്രമിക്കാം.
മെമ്മറിയെക്കുറിച്ച് ചിന്തിക്കുന്നു
ഫംഗ്ഷന്റെ മെമ്മറി ഉപയോഗം മെച്ചപ്പെടുത്തുന്നതിന്, നമുക്ക് ബ്ലോക്ക്-ബൈ-ബ്ലോക്ക് ആയി പ്രവർത്തിക്കാം, ചെറിയ ചങ്കുകളിൽ ഒരേ പ്രവർത്തനങ്ങൾ ചെയ്യുക, ഒടുവിൽ എല്ലാം കൂട്ടിച്ചേർക്കുക. ഇത് ഒരു സമയം ഒരു ചങ്ക് മാത്രം മെമ്മറിയിൽ സൂക്ഷിക്കാൻ അനുവദിക്കും, കൂടാതെ Numpy യുടെ വെക്റ്ററൈസ്ഡ് കോഡിൽ നിന്ന് ലഭിക്കുന്ന ഗുണം നിലനിർത്തിക്കൊണ്ട് കാഷെ ഉപയോഗം മെച്ചപ്പെടുത്താനും സാധ്യതയുണ്ട്.
ന്റെ ഒരു ശ്രേണി എടുക്കാൻ ഫംഗ്ഷൻ പരിഷ്കരിക്കാം:
# [N1, N2], inclusive
def basel_np_range(N1: int, N2: int) -> float:
# ടൈമിംഗ് കോഡ് ഒഴിവാക്കി
ones = 1
r = np.arange(N1, N2 + 1)
div = ones / r
squares = np.square(div)
return float(np.sum(squares))
എല്ലാ ചങ്കുകളും കൂട്ടിച്ചേർക്കാൻ ഒരു ഫംഗ്ഷൻ എഴുതാം.
def basel_chunks(N: int, chunk_size: int) -> float:
# ടൈമിംഗ് കോഡ് ഒഴിവാക്കി
s = 0.0
num_chunks = N // chunk_size
for i in range(num_chunks):
s += basel_np_range(i * chunk_size + 1, (i + 1) * chunk_size)
return s
ഉപയോഗിച്ച്:
Function 'basel_chunks' executed in 0.108557 seconds.
1.6449340568482258
കൊള്ളാം! ഇത് Numpy പരിഹാരത്തേക്കാൾ മടങ്ങ് വേഗത്തിൽ പ്രവർത്തിക്കുന്നു.
ഒരു പ്ലസ് ആയി, IEEE 754 ഫ്ലോട്ടുകളുടെ സ്വഭാവം കാരണം ഉത്തരം യഥാർത്ഥത്തിൽ അൽപ്പം കൂടുതൽ കൃത്യമായിരിക്കും. മുമ്പ് ചെയ്തതുപോലെ പ്രകടനം എങ്ങനെ സ്കെയിൽ ചെയ്യുന്നുവെന്ന് നോക്കാം, ചങ്ക് വലുപ്പം സ്ഥിരമായി നിലനിർത്തിക്കൊണ്ട്.
ആദ്യം, y-അക്ഷം നോക്കുക. ഇപ്പോൾ നമ്മൾ മിനിറ്റുകളുടെ അളവിലല്ല, സെക്കൻഡുകളുടെ അളവിലാണ് പ്രവർത്തിക്കുന്നത്. റൺടൈം രേഖീയമായി വർദ്ധിക്കുന്നതായി നമ്മൾ കാണുന്നു, അൽഗോരിതത്തിന്റെ
സമയ സങ്കീർണ്ണതയുമായി യോജിക്കുന്നു. chunk_size
20000 ആയിരിക്കുമ്പോൾ, എല്ലാ വിവരങ്ങളും കാഷെയിൽ ഫിറ്റ് ചെയ്യാൻ കഴിയുമെന്ന് നമുക്ക് സിദ്ധാന്തിക്കാം, അതിനാൽ കാഷെയ്ക്ക് പുറത്തുള്ള ആക്സസ്സിന് റൺടൈം സ്പൈക്കുകൾ ഉണ്ടാകുന്നില്ല.
സ്ഥിരമായി
ആയി നിലനിർത്തിക്കൊണ്ട് chunk_size
ശ്രേണിയിൽ മാറ്റാൻ ശ്രമിക്കാം.
ചിത്രത്തിൽ നിന്ന്, chunk_size
വരെ പ്രകടന ലാഭങ്ങൾ കാണുന്നു, അതിനുശേഷം കാഷെ മിസുകൾ മൂലമുണ്ടാകുന്ന ലാറ്റൻസി സ്പൈക്കുകൾ കാണാം.
ചില നാപ്കിൻ മാത്ത്
ഓരോ float64
ഉം 64 ബിറ്റുകൾ, അല്ലെങ്കിൽ 8 ബൈറ്റുകൾ ഉപയോഗിക്കുന്നു. ഞങ്ങൾ
float64
കളുടെ 3 അറേകൾ ഉപയോഗിച്ച് പ്രവർത്തിക്കുന്നു, അതിനാൽ ഒരു കോളിന് മെമ്മറി ഉപയോഗം
MB ആണ്.
എന്റെ M1 Pro യുടെ സ്പെസിഫിക്കേഷനുകൾ ഇതാണ്:
മെമ്മറി തരം | വലുപ്പം |
---|---|
L1 | 128KB (ഡാറ്റാ കാഷെ, പെർ കോർ) |
L2 | 24MB (6 പെർഫോമൻസ് കോറുകൾക്കിടയിൽ പങ്കിടുന്നു) |
L3 | 24MB |
മെയിൻ | 16GB |
ഇത് സൂചിപ്പിക്കുന്നത് ഫംഗ്ഷന് ലഭ്യമായ പരമാവധി കാഷെ സ്പേസ് MB ആണ്, അതിനപ്പുറം പോയാൽ കാഷെയ്ക്കായി കാത്തിരിക്കേണ്ടിവരും.
മൾട്ടിപ്രോസസിംഗ്
ഇനി, കമ്പ്യൂട്ടർ അറേ കാഷെയിൽ കൂടുതൽ ഫിറ്റ് ചെയ്യാൻ ശ്രമിക്കാം. ഒരു സാധ്യത എല്ലാ കോറുകളിലും ഫംഗ്ഷൻ റൺ ചെയ്യാൻ ശ്രമിക്കുക എന്നതാണ്, അതിനാൽ നമുക്ക് കൂടുതൽ ഫംഗ്ഷനായി ഉപയോഗിക്കാം, അതിനാൽ അത് ശ്രമിക്കാം.
ആദ്യം, basel_chunks
-നെ
ന്റെ ഒരു റേഞ്ച് ഇൻപുട്ടായി എടുക്കാൻ മാറ്റാം.
# (N1, N2]
def basel_chunks_range(N1: int, N2: int, chunk_size: int):
# ടൈമിംഗ് കോഡ് ഒഴിവാക്കി
s = 0.0
num_chunks = (N2 - N1) // chunk_size
for i in range(num_chunks):
s += basel_np_range(N1 + i * chunk_size + 1, N1 + (i + 1) * chunk_size)
return s
ഇനി യഥാർത്ഥ മൾട്ടിപ്രോസസിംഗ്:
from multiprocessing import Pool
def basel_multicore(N: int, chunk_size: int):
# എന്റെ മെഷീനിൽ 10 കോറുകൾ
NUM_CORES = 10
N_PER_CORE = N // NUM_CORES
Ns = [
(i * N_PER_CORE, (i + 1) * N_PER_CORE, chunk_size)
for i in range(NUM_CORES)
]
# 10 ബാച്ചുകൾ സമാന്തരമായി പ്രോസസ് ചെയ്യുക
with Pool(NUM_CORES) as p:
result = p.starmap(basel_chunks_range, Ns)
return sum(result)
ഹുഡിന് കീഴിൽ, പൈത്തണിന്റെ മൾട്ടിപ്രോസസിംഗ്
മൊഡ്യൂൾ പിക്കിൽ
ഫംഗ്ഷൻ ഒബ്ജക്റ്റ്, കൂടാതെ 9 പുതിയ python3.10
ഇൻസ്റ്റൻസുകൾ സൃഷ്ടിക്കുന്നു, ഇവയെല്ലാം
, num_chunks = 50000
എന്നിവ ഉപയോഗിച്ച് ഫംഗ്ഷൻ എക്സിക്യൂട്ട് ചെയ്യുന്നു.
പ്രകടനം താരതമ്യം ചെയ്യാം.
Function 'basel_multicore' executed in 1.156558 seconds.
1.6449340658482263
Function 'basel_chunks' executed in 1.027350 seconds.
1.6449340658482263
അയ്യോ, ഇത് മോശമായി പ്രവർത്തിച്ചു. ഇത് മിക്കവാറും മൾട്ടിപ്രോസസിംഗ് ചെയ്യുന്ന പ്രാരംഭ പ്രവൃത്തി ചെലവേറിയതിനാൽ ആയിരിക്കാം, അതിനാൽ ഇത് ഒരു ഫലം നൽകുന്നത് ഫംഗ്ഷൻ പൂർത്തിയാക്കാൻ താരതമ്യേന കൂടുതൽ സമയമെടുക്കുമ്പോൾ മാത്രമാണ്.
ആയി വർദ്ധിപ്പിക്കുമ്പോൾ:
Function 'basel_multicore' executed in 2.314828 seconds.
1.6449340667482235
Function 'basel_chunks' executed in 10.221904 seconds.
1.6449340667482235
അതാ, അത് വന്നു! ഇത് മടങ്ങ് വേഗത്തിലാണ്. ശ്രമിക്കാം
Function 'basel_multicore' executed in 13.844876 seconds.
multicore 1.6449340668379773
Function 'basel_chunks' executed in 102.480372 seconds.
chunks 1.6449340668379773
അത് മടങ്ങ് വേഗത്തിലാണ്! വർദ്ധിക്കുമ്പോൾ, സിംഗിൾ-കോർ മുതൽ 10-കോർ വരെയുള്ള അനുപാതം 10:1 ആയി സമീപിക്കണം.
x-അക്ഷം ഒരു ലോഗ് സ്കെയിലിലാണ്, അതിനാലാണ് ലീനിയർ ആയി വർദ്ധിക്കുന്ന റൺടൈമുകൾ എക്സ്പോണൻഷ്യൽ ആയി കാണുന്നത്
മൾട്ടിപ്രോസസിംഗ് ഉപയോഗിക്കുന്നതിന് സെക്കൻഡ് ഓവർഹെഡ് ഉണ്ടെന്ന് നമുക്ക് കാണാം, അതിനാൽ ചങ്ക് ചെയ്ത റൺടൈം കൂടുതൽ ആയിരിക്കുമ്പോൾ മാത്രമേ പ്രകടന ഗുണം ഉണ്ടാകൂ. ഇത് ആയി സംഭവിക്കുന്നു. എന്നിരുന്നാലും, അതിനുശേഷം, വലിയ വ്യത്യാസമുണ്ട്.
പരീക്ഷണത്തിലൂടെ
(ഇവിടെ കാണിച്ചിട്ടില്ല) ഒരു chunk_size
ഏകദേശം 50000 ആണ് എന്ന് ഞാൻ കണ്ടെത്തി
മൾട്ടിപ്രോസസിംഗ് കോഡിനും ഒപ്റ്റിമൽ ആണ്, ഇത് ഓപ്പറേറ്റിംഗ് സിസ്റ്റം
ഒരു കോറിന് L2 ഉപയോഗത്തിൽ ചില നിയന്ത്രണങ്ങൾ ഏർപ്പെടുത്തുന്നുവെന്ന് നമ്മോട് പറയുന്നു.
ഫലങ്ങളുടെ സംഗ്രഹം
പൈത്തൺ ഫംഗ്ഷൻ | (സെക്കൻഡ്) | (സെക്കൻഡ്) | (സെക്കൻഡ്) |
---|---|---|---|
basel_multicore |
1.04 | 1.17 | 2.33 |
basel_chunks |
0.09 | 0.91 | 9.18 |
basel_np_broadcast |
0.27 | 9.68 | NaN (മെമ്മറി തീർന്നു) |
basel_less_pythonic |
5.99 | 59.25 | 592 (ഏകദേശം) |
basel (സാധാരണ) |
7.64 | 68.28 | 682 (ഏകദേശം) |
basel_np |
0.618 | 44.27 | NaN (മെമ്മറി തീർന്നു) |
മുമ്പത്തെ പോസ്റ്റിലെ ഭാഷകളുമായി ഇത് എങ്ങനെ താരതമ്യപ്പെടുത്താം?
ഭാഷ | (മില്ലിസെക്കൻഡ്) |
---|---|
Rust (rc, –release, multicore w/ rayon) | 112.65 |
Python3.10 (basel_chunks ) |
913 |
Rust (rc, –release) | 937.94 |
C (clang, -O3) | 995.38 |
Python3.10 (basel_multicore ) |
1170 |
Python3.10 (basel_np_broadcast ) |
9680 |
Haskell (ghc, -O3) | 13454 |
Python3.10 (basel_np ) |
44270 |
Python3.10 (basel_less_pythonic ) |
59250 |
Python3.10 (basel ) |
68280 |
വളരെ നല്ലത്! ചങ്ക് ചെയ്ത Numpy കോഡ് ഏറ്റവും വേഗതയേറിയ സീക്വൻഷ്യൽ പരിഹാരമാണ്! ഓർക്കുക, പൈത്തൺ കണക്കുകൂട്ടലുകൾ നടത്തുമ്പോൾ ഒരു ഇന്റർപ്രെറ്റർ പശ്ചാത്തലത്തിൽ പ്രവർത്തിക്കുന്നു.
Rust-നെ പൈത്തൺ മൾട്ടികോറുമായി താരതമ്യം ചെയ്യുമ്പോൾ:
ഭാഷ | (മില്ലിസെക്കൻഡ്) | (മില്ലിസെക്കൻഡ്) | (മില്ലിസെക്കൻഡ്) | (മില്ലിസെക്കൻഡ്) |
---|---|---|---|---|
Rust | 12.3 | 111.2 | 1083 | 10970 |
Python | 1040 | 1173 | 2330 | 12629 |
വ്യക്തമായും, ചെറിയ -ന് Rust-ന്റെ സമാന്തരീകരണ രീതി (OS ത്രെഡുകൾ വഴി) പൈത്തണിന്റെ പ്രോസസ് അടിസ്ഥാനമാക്കിയ സമാന്തരീകരണത്തേക്കാൾ കൂടുതൽ കാര്യക്ഷമമാണ്. എന്നാൽ വർദ്ധിക്കുമ്പോൾ രണ്ടിനും ഇടയിലുള്ള വ്യത്യാസം കുറയുന്നു.
ഉപസംഹാരം
ഇത് വ്യക്തമല്ലെങ്കിൽ, ഈ ലേഖനം മുഴുവൻ പൈത്തൺ എങ്ങനെ പ്രവർത്തിക്കുന്നു എന്ന് അൽപ്പം മനസ്സിലാക്കാൻ ഒരു പരിശീലനമായിരുന്നു. ദയവായി നിങ്ങളുടെ മാനേജറെ പ്രഭാവിതമാക്കാൻ പൈത്തൺ ഹൈപ്പർ-ഒപ്റ്റിമൈസ് ചെയ്യാൻ ശ്രമിക്കരുത്. എല്ലായ്പ്പോഴും മികച്ച പരിഹാരമുണ്ട്, ഉദാഹരണത്തിന്:
from math import pi
def basel_smart() -> float:
return pi*pi / 6
താ-ദാ! മറ്റെല്ലാ ഫംഗ്ഷനുകളേക്കാളും വളരെ വേഗത്തിലും അനന്തമായി ലളിതവുമാണ്.
പ്രകടനത്തെക്കുറിച്ച് ചിന്തിക്കുമ്പോൾ, നിങ്ങൾ ശ്രദ്ധാലുവായിരിക്കേണ്ടതുണ്ട്. നിങ്ങൾ അളന്നതിന് ശേഷം, പ്രകടനം ഒരു പ്രശ്നമാണെന്ന് നിങ്ങൾക്ക് ഉറപ്പുണ്ടെങ്കിൽ മാത്രം അത് ഒരു പ്രശ്നമാണെന്ന് അനുമാനിക്കരുത്. അങ്ങനെയാണെങ്കിൽ, ഒരു ലളിതമായ പ്രശ്നത്തിന് import multiprocessing
എന്ന് ടൈപ്പ് ചെയ്യാനും ബ്രൂട്ട് ഫോഴ്സ് ചെയ്യാനും മുമ്പ്, പ്രശ്നം പരിഹരിക്കാൻ മികച്ച ഒരു വഴിയുണ്ടോ എന്ന് എല്ലായ്പ്പോഴും പരിശോധിക്കുക.
കൂടാതെ, നിങ്ങളുടെ ആപ്ലിക്കേഷൻ ശരിക്കും പ്രകടനത്തെ അടിസ്ഥാനമാക്കിയുള്ളതാണെങ്കിൽ, നിങ്ങൾ അത് പൈത്തണിൽ എഴുതുന്നത് ശരിയല്ല. പ്രോട്ടോടൈപ്പിംഗിൽ മികച്ച പ്രകടനം നൽകുന്ന ഒരു ഉപകരണമായാണ് പൈത്തൺ സൃഷ്ടിച്ചത്, എക്സിക്യൂഷൻ സ്പീഡ് അല്ല. എന്നാൽ നിങ്ങൾക്ക് കാണാനാകുന്നതുപോലെ, മികച്ച പ്രകടനം നേടുന്നത് അസാധ്യമല്ല.
എന്തായാലും, ഈ സൈറ്റിലെ എന്റെ ആദ്യത്തെ യഥാർത്ഥ ലേഖനം നിങ്ങൾ ആസ്വദിച്ചുവെന്ന് ഞാൻ പ്രതീക്ഷിക്കുന്നു. ഈ “പ്രശ്നത്തിന്” വേഗതയേറിയ പൈത്തൺ “പരിഹാരം” കണ്ടെത്തിയാൽ, എനിക്ക് ഒരു ഇമെയിൽ അയയ്ക്കുക!
അപ്ഡേറ്റ് ലോഗ്
തീയതി | വിവരണം | നന്ദി |
---|---|---|
08/02/2023 | ബ്രോഡ്കാസ്റ്റിംഗ് വിഭാഗം ചേർത്തു, പൈത്തൺ ഏറ്റവും വേഗതയേറിയ പരിഹാരമായി മാറി | u/mcmcmcmcmcmcmcmcmc_ & u/Allanon001 |
08/02/2023 | time.time() എന്നത് time.perf_counter() ആക്കി മാറ്റി |
u/james_pic |