• യഥാർത്ഥ പരിഹാരം
  • നമുക്ക് നമ്പൈ ഉപയോഗിക്കാം
    • ബ്രോഡ്കാസ്റ്റിംഗ്
  • മെമ്മറിയെക്കുറിച്ച് ചിന്തിക്കുന്നു
  • കണക്കുകൂട്ടലുകള്‍
  • മൾട്ടിപ്രോസസിംഗ്
  • ഫലങ്ങളുടെ സംഗ്രഹം
  • ഉപസംഗ്രഹം
  • നാൾപ്പതിപ്പ്
  • ഹോം
  • പോസ്റ്റുകൾ
  • കുറിപ്പുകൾ
  • പുസ്തകശാല
  • രചയിതാവ്
🇺🇸 en 🇫🇷 fr 🇨🇳 zh

Nathaniel Thomas

പൈത്തണിൽ അത്രയും സാധാരണമല്ലാത്ത പെർഫോമൻസ് ഒപ്റ്റിമൈസേഷൻ

2023, ഓഗസ്റ്റ് 1

എന്റെ മുമ്പത്തെ പോസ്റ്റ് (സത്യത്തിൽ ഈ സൈറ്റിന്റെ തീം പരീക്ഷിക്കാനായിരുന്നു), വിപരീത വർഗ്ഗങ്ങളുടെ തുകയുടെ N പദങ്ങൾ കണക്കാക്കിയ ചില കോഡ് സ്നിപ്പെറ്റുകൾ അടങ്ങിയിരുന്നു. എന്റെ 4 പ്രിയപ്പെട്ട ഭാഷകളായ—പൈത്തൺ, സി, റസ്റ്റ്, ഹാസ്കെൽ—എന്നിവയിലാണ് ഞാൻ കോഡ് എഴുതിയത്, പക്ഷേ ഞാൻ പൈത്തൺ കോഡ് റൺ ചെയ്തപ്പോൾ, അത് നാണംകെടുത്തുന്ന രീതിയിൽ മന്ദഗതിയിലായിരുന്നു. സീക്വൻഷ്യൽ റസ്റ്റിന് എടുത്ത ≈950 ms നേരത്തിന് നേരെ, പൈത്തൺ 70 സെക്കൻഡ് എടുത്തു! അതിനാൽ, ഈ പോസ്റ്റിൽ, പൈത്തണിന് കുറച്ച് കൂടി സഹനീയമായ സംഖ്യകൾ നേടിക്കൊടുക്കാൻ ഞങ്ങൾ ശ്രമിക്കും.

യഥാർത്ഥ പരിഹാരം

def basel(N: int) -> float:
    return sum(x**(-2) for x in range(1,N))

ഇതാണ് ഈ ജോലി ചെയ്യാനുള്ള ഏറ്റവും പൈത്തോണിക് വഴി. ബിൽറ്റ്-ഇൻ ജനറേറ്ററുകൾ ഉപയോഗിക്കുന്ന, വായിക്കാൻ എളുപ്പമുള്ള ഒറ്റ വരി കോഡ്. നമുക്ക് സമയം മാപ്പ് ചെയ്യാം, കഴിഞ്ഞ പോസ്റ്റിൽ ഉണ്ടായിരുന്നത് പോലെ 109 അല്ല N=108 ഉപയോഗിച്ച് (അത്ര നേരം കാത്തിരിക്കാൻ ഞാൻ ആഗ്രഹിക്കാത്തതിനാൽ):

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` ലൂപ്പ് യീൽഡ് ഉപയോഗിച്ച്
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 ഡയറക്ടീവുകൾ ആവശ്യമാണ്. പ്രായോഗികമായി, അവയുടെ സോമനശീലം കാരണം ജനറേറ്ററുകൾ സാധാരണയായി വേഗതയുള്ളതാണ് എന്നത് ശ്രദ്ധിക്കുക. ഈ സാഹചര്യത്തിൽ മാത്രം, അധിക ബ്ലോട്ട് അതിന്റെ മൂല്യം വഹിക്കുന്നില്ല.

എന്തായാലും, രണ്ട് പതിപ്പുകൾക്കിടയിലുള്ള പ്രകടന വ്യത്യാസം ( ≈1s) പൈത്തൺ, സി/റസ്റ്റ് എന്നിവയ്ക്കിടയിലുള്ള മൊത്തത്തിലുള്ള വ്യത്യാസത്തോട് താരതമ്യം ചെയ്യുമ്പോൾ നിസ്സാരമാണ്. വേഗതയേറിയ പതിപ്പ് ഉപയോഗിച്ചാലും, റസ്റ്റ് കോഡ് പൈത്തണിനേക്കാൾ ≈65 മടങ്ങ് വേഗമുള്ളതാണ്.

എന്തുകൊണ്ട്? പ്രധാനമായും കാരണം പൈത്തൺ ഒരു ശിഥിലമായ ടൈപ്പ് ചെയ്ത, ഇന്റർപ്രെറ്റ് ചെയ്യപ്പെട്ട ഭാഷയാണ്. ഇതിനർത്ഥം പൈത്തൺ ഇന്റർപ്രെറ്റർ (സിപൈത്തൺ), പൈത്തൺ കോഡ് നിങ്ങളുടെ കമ്പ്യൂട്ടർ എളുപ്പത്തിൽ എക്സിക്യൂട്ട് ചെയ്യാൻ കഴിയുന്ന ഒന്നാക്കി മാറ്റേണ്ടതുണ്ട്, തത്സമയം. പൈത്തൺ കോഡ് ഒരു സാമാന്യവൽക്കരിച്ച ഫോമിലുള്ള അസംബ്ലിയായി വേഗത്തിൽ കംപൈൽ ചെയ്യുന്നതിലൂടെയാണ് ഇത് ചെയ്യുന്നത്, പൈത്തൺ ബൈറ്റ്കോഡ് എന്ന് വിളിക്കപ്പെടുന്ന, അത് നമ്മൾ ഇപ്പോൾ നോക്കി.

ഇത് ഇന്റർപ്രെറ്റർ എക്സിക്യൂട്ട് ചെയ്യുന്നു. ഈ കംപൈലേഷൻ ഘട്ടം പൈത്തൺ അനുഭവിക്കുന്ന ആദ്യത്തെ പ്രകടന പ്രശ്നമാണിത്. റസ്റ്റ് പോലുള്ള ഭാഷകൾ ഒരു പ്രോഗ്രാം ഒരു തവണ മാത്രം കംപൈൽ ചെയ്യേണ്ടതുള്ളതിനാൽ, ആ സമയം ഒരു പ്രോഗ്രാമിന്റെ റൺടൈമിൽ ഉൾപ്പെടുത്തിയിട്ടില്ല. എന്നാൽ പ്രകടനത്തിന് ഏറ്റവും വലിയ പ്രശ്നം തരംതാഴ്ന്ന നിലവാരമുള്ള, ആർക്കിടെക്ചർ അജ്ഞാതമായ, സ്വദേശിയായി ഒപ്റ്റിമൈസ് ചെയ്തിട്ടില്ലാത്ത ബൈറ്റ്കോഡിൽ നിന്നാണ് വരുന്നത്. ഇത് വെറുതേ ഇന്റർപ്രെറ്റ് ചെയ്യപ്പെട്ട ഭാഷകളുടെ സ്വഭാവത്തിന്റെ ഒരു ഭാഗമാണ്, കാരണം അവയ്ക്ക് ഉയർന്ന നിലവാരമുള്ള കംപൈലേഷനിൽ വളരെയധികം സമയം ചെലവഴിക്കാൻ കഴിയില്ല.

അപ്പോൾ, നിങ്ങൾക്ക് എങ്ങനെ വേഗതയേറിയ പൈത്തൺ എഴുതാം? ശരി, നിങ്ങൾക്ക് കഴിയില്ല. പക്ഷേ, നിങ്ങൾക്ക് കോൾ ചെയ്യാം വേഗതയേറിയ സി കോഡിലേക്ക്, നമ്പി പോലുള്ള കനത്ത ഒപ്റ്റിമൈസേഷൻ ഉള്ള ലൈബ്രറികളിലൂടെ. ഈ ലൈബ്രറികളിൽ പ്രീ-കംപൈൽ ചെയ്ത, വെക്റ്ററൈസ് ചെയ്ത സി ഫങ്ഷനുകൾ അടങ്ങിയിരിക്കുന്നു, അവ പൈത്തൺ ഇന്റർപ്രെറ്റർ മുഴുവനും ഒഴിവാക്കാൻ നിങ്ങളെ അനുവദിക്കുന്നു.

നമുക്ക് നമ്പൈ ഉപയോഗിക്കാം

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

വാവ്, ഇത് ≈13 മടങ്ങ് വേഗത്തിൽ പ്രവർത്തിച്ചു! ഈ സാഹചര്യത്തിൽ ബൈറ്റ്കോഡ് നോക്കിയാൽ വലിയ തെളിവൊന്നും കിട്ടില്ല, കാരണം അത് നമ്പൈയിലേക്കുള്ള കുറച്ച് CALL_FUNCTION കോളുകൾ മാത്രമായിരിക്കും, യഥാർത്ഥ ജോലി ചെയ്യുന്നത് നമ്പൈയാണ്. ഏത് വരിയാണ് ഏറ്റവും കൂടുതൽ സമയം എടുക്കുന്നതെന്ന് നോക്കാം:

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
ഓപ്പറേഷൻ ഓപ്പറേഷന് വേണ്ട സമയം (മില്ലിസെക്കൻഡ്) ആകെ സമയം (മില്ലിസെക്കൻഡ്)
ഒന്നുകൾ സൃഷ്ടിക്കൽ 97 97
ശ്രേണി സൃഷ്ടിക്കൽ 79 176
ശ്രേണി വർഗ്ഗീകരണം 98 274
ഒന്നുകൾ/വർഗ്ഗങ്ങൾ ഹരിക്കൽ 112 387
അന്തിമ തുക 58 444

ഈ സംഖ്യകളെക്കുറിച്ച് ഒരു നിമിഷം ചിന്തിക്കാം. ഒന്നുകൾ/ശ്രേണി സൃഷ്ടിക്കൽ ഘട്ടങ്ങളിൽ കനത്ത കമ്പ്യൂട്ടേഷണൽ ജോലി ഇല്ല. എന്നിട്ടും, വർഗ്ഗീകരണം, ഹരിക്കൽ തുടങ്ങിയ “കഠിന” ഘട്ടങ്ങൾക്ക് തുല്യമായ സമയമാണ് അവ എടുക്കുന്നത്. അത്ഭുതകരമെന്നു പറയട്ടെ, അന്തിമ അറേയുടെ തുക കണ്ടുപിടിക്കുന്നതാണ് ഏറ്റവും വേഗത്തിലുള്ള ഘട്ടം! ഇത് സൂചിപ്പിക്കുന്നത് ഇവിടെ ബോട്ടിൽനെക്ക് സിപിയു അല്ല, മെമ്മറി ആക്സസ് ആണെന്നാണ്. N 107 മുതൽ 109 വരെ മാറ്റിവെച്ചുകൊണ്ട് പ്രകടനം എങ്ങനെ സ്കെയിൽ ചെയ്യുന്നുവെന്ന് നോക്കാം.

NumPy പ്രവർത്തനരീതിയുടെ ഇൻപുട്ട് വലുപ്പങ്ങളിലുടനീളമുള്ള പ്രവർത്തന സമയ വിശകലനം
NumPy പ്രവർത്തനരീതിയുടെ ഇൻപുട്ട് വലുപ്പങ്ങളിലുടനീളമുള്ള പ്രവർത്തന സമയ വിശകലനം

ഈ പ്ലോട്ടിൽ, ഏറ്റവും മുകളിലത്തെ വരിയുടെ അറ്റം മൊത്തം എടുത്ത സമയത്തെ പ്രതിനിധീകരിക്കുന്നു, തുടർച്ചയായ വരികൾക്കിടയിലുള്ള ഇടം ഓരോ ഘട്ടത്തിന്റെയും സമയത്തെ പ്രതിനിധീകരിക്കുന്നു.

നമുക്ക് കാണാം:

  1. അൽഗോരിതം O(n) ആണെങ്കിലും, മൊത്തം സമയം നോൺ-ലീനിയർ ആയി കൂടുന്നു.

  2. ഹരിക്കൽ ഘട്ടമാണ് ഏറ്റവും കൂടുതൽ സമയം എടുക്കുന്നത്, ≈3×108, ≈7×108 എന്നിവയിൽ കൂർത്ത വർദ്ധനവോടെ.

ഇത് അർത്ഥവത്താണ്, കാരണം മറ്റ് ഓപ്പറേഷനുകളിൽ നിന്ന് വ്യത്യസ്തമായി, np.divide രണ്ട് വലിയ അറേകൾ ഒരേസമയം ആക്സസ് ചെയ്യേണ്ടതുണ്ട്, ഫലം ഒരു പുതിയ അറേയിലേക്ക് എഴുതേണ്ടതുണ്ട്. ഇതിനർത്ഥം ധാരാളം പ്രധാന മെമ്മറി ആക്സസുകളും, എസ്എസ്ഡിയിൽ നിന്നുള്ള വായനകളും ആവശ്യമാണ്, അത് വളരെ മന്ദഗതിയിലാണ് സംഭവിക്കുന്നത്.

ബ്രോഡ്കാസ്റ്റിംഗ്

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))

ഇത് വിലപ്പെട്ട കാഷെ സ്ഥലം ലാഭിക്കാൻ നമ്മെ സഹായിക്കുന്നു. നമുക്ക് മുമ്പത്തെ അതേ മെട്രിക്സ് ഇപ്പോൾ റൺ ചെയ്യാം. N=108 ഉപയോഗിച്ച്:

ഓപ്പറേഷൻ ഓപ്പറേഷന്‍ വേണ്ട സമയം (മില്ലിസെക്കൻഡ്) ആകെ സമയം (മില്ലിസെക്കൻഡ്)
ones സൃഷ്ടിക്കൽ 0.00 0
range സൃഷ്ടിക്കൽ 68.56 70.48
range വർഗ്ഗമാക്കൽ 105.14 180.74
ones/squares ഹരിക്കൽ 133.08 271.30
അവസാന സംഖ്യകളുടെ തുക 71.08 310.41
ബ്രോഡ്കാസ്റ്റ് ചെയ്ത NumPy അപ്രോച്ചിന്റെ റൺടൈം വിഭജനം
ബ്രോഡ്കാസ്റ്റ് ചെയ്ത NumPy അപ്രോച്ചിന്റെ റൺടൈം വിഭജനം

ഇനി മുതൽ, ബ്രോഡ്കാസ്റ്റ് ചെയ്ത പതിപ്പിനെ “Numpy സൊല്യൂഷൻ” എന്ന് വിളിക്കാം.

കാര്യമായ മെച്ചപ്പെടുത്തലായിരുന്നെങ്കിലും, ആ ലാറ്റൻസി സ്പൈക്കുകൾ ഇപ്പോഴും കാണാം. നമുക്ക് അത് പരിഹരിക്കാൻ ശ്രമിക്കാം.

മെമ്മറിയെക്കുറിച്ച് ചിന്തിക്കുന്നു

ഫങ്ഷന്റെ മെമ്മറി ഉപയോഗം മെച്ചപ്പെടുത്താൻ, നമുക്ക് ബ്ലോക്ക്-ബൈ-ബ്ലോക്ക് ആയി പ്രവർത്തിക്കാം, ചെറിയ ഭാഗങ്ങളിൽ ഒരേ ഓപ്പറേഷനുകൾ നടത്തി, അവസാനം എല്ലാം കൂട്ടിച്ചേർക്കുക. ഇത് ഒരു സമയം ഒരു ചങ്ക് മാത്രം മെമ്മറിയിൽ സൂക്ഷിക്കാൻ നമ്മെ അനുവദിക്കും, കൂടാതെ കാഷെ ഉപയോഗം മെച്ചപ്പെടുത്തുമെന്ന് പ്രതീക്ഷിക്കാം, അതേസമയം Numpy-യുടെ വെക്ടറൈസ്ഡ് കോഡിന്റെ പ്രയോജനം നിലനിർത്താം.

Nകളുടെ ഒരു ശ്രേണി എടുക്കാൻ ഫങ്ഷൻ പരിഷ്കരിക്കാം:

# [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

N=108 ഉപയോഗിച്ച്:

Function 'basel_chunks' executed in 0.108557 seconds.
1.6449340568482258

കൊള്ളാം! ഇത് Numpy പരിഹാരത്തേക്കാൾ ≈3 മടങ്ങ് വേഗത്തിൽ പ്രവർത്തിക്കുന്നു.

ഒരു അധിക ഗുണമായി, IEEE 754 ഫ്ലോട്ടുകളുടെ സ്വഭാവം കാരണം ഉത്തരം യഥാർത്ഥത്തിൽ കുറച്ച് കൂടി കൃത്യമായിരിക്കും. മുമ്പ് ചെയ്തതുപോലെ തന്നെ പ്രകടനം എങ്ങനെ സ്കെയിൽ ചെയ്യുന്നു എന്ന് നോക്കാം, ചങ്ക് സൈസ് സ്ഥിരമായി നിലനിർത്തിക്കൊണ്ട്.

വ്യത്യസ്ത ഇൻപുട്ട് സൈസുകൾക്കുള്ള ചങ്ക് ചെയ്ത NumPy റൺടൈം
വ്യത്യസ്ത ഇൻപുട്ട് സൈസുകൾക്കുള്ള ചങ്ക് ചെയ്ത NumPy റൺടൈം

ഒന്നാമതായി, y-അക്ഷം നോക്കൂ. ഇപ്പോൾ നമ്മൾ മിനിറ്റുകളുടെ അളവിലല്ല, സെക്കൻഡുകളുടെ അളവിലാണ് പ്രവർത്തിക്കുന്നത്. റൺടൈം രേഖീയമായി വർദ്ധിക്കുന്നതും നാം കാണുന്നു, അൽഗോരിതത്തിന്റെ O(n) സമയ സങ്കീർണ്ണതയുമായി ഇത് സ്ഥിരമാണ്. 20000 എന്ന chunk_size ഉപയോഗിച്ച്, എല്ലാ വിവരങ്ങളും കാഷെയിൽ ഫിറ്റ് ചെയ്യാൻ കഴിയുമെന്ന് നമുക്ക് സിദ്ധാന്തിക്കാം, അതിനാൽ കാഷെയിൽ നിന്ന് പുറത്തുള്ള ആക്സസ്സിന് റൺടൈം സ്പൈക്കുകളൊന്നുമില്ല.

chunk_size [5×102,106] ശ്രേണിയിൽ വ്യത്യാസപ്പെടുത്തി നോക്കാം, N 109 എന്ന സ്ഥിരമൂല്യത്തിൽ നിലനിർത്തിക്കൊണ്ട്.

N = 10^9-ന് ചങ്ക് സൈസ് വ്യത്യാസപ്പെടുമ്പോഴുള്ള റൺടൈം
N = 10^9-ന് ചങ്ക് സൈസ് വ്യത്യാസപ്പെടുമ്പോഴുള്ള റൺടൈം

ചിത്രത്തിൽ നിന്ന്, ≈51000 chunk_size വരെ പ്രകടന ലാഭം നാം കാണുന്നു, അതിനുശേഷം കാഷെ മിസുകൾ കാരണം സംഭവിക്കുന്ന ലാറ്റൻസി സ്പൈക്കുകൾ ഉണ്ട്.

കണക്കുകൂട്ടലുകള്‍

കാഷെ ഹൈരാര്‍ക്കി

ഓരോ float64 ഉം 64 ബിറ്റുകള്‍ അല്ലെങ്കില്‍ 8 ബൈറ്റുകള്‍ ഉപയോഗിക്കുന്നു. നമ്മള്‍ N float64 മൂല്യങ്ങളുള്ള 3 അറേകളാണ് പ്രവര്‍ത്തിക്കുന്നത്, അതിനാല്‍ ഒരു കോള്‍ ഉപയോഗിക്കുന്ന മെമ്മറി 51000(8)(3)=1224000≈1.2 എംബി ആണ്.

എന്റെ എം1 പ്രോയുടെ സ്പെക്കുകള്‍ ഇവയാണ്:

മെമ്മറി തരം വലുപ്പം
എല്‍1 128കെബി (ഡേറ്റാ കാഷെ, പെര്‍ കോര്‍)
എല്‍2 24എംബി (6 പെര്‍ഫോമന്‍സ് കോറുകള്‍ക്കിടയില്‍ പങ്കുവെക്കുന്നത്)
എല്‍3 24എംബി
പ്രധാന 16ജിബി

ഇത് സൂചിപ്പിക്കുന്നത് ഫങ്ഷന്‍ ഉപയോഗിക്കാന്‍ കഴിയുന്ന പരമാവധി L1+L2 കാഷെ സ്ഥലം ≈1.2 എംബി ആണ്, അതില്‍ കൂടുതല്‍ പോയാല്‍ നമുക്ക് L3 കാഷെക്കായി കാത്തിരിക്കേണ്ടി വരും.

മൾട്ടിപ്രോസസിംഗ്

ഇനി, കമ്പ്യൂട്ടറിന് അറേയുടെ കൂടുതൽ ഭാഗം കാഷെയിൽ ഫിറ്റ് ചെയ്യാൻ കഴിയുന്നത് പരീക്ഷിക്കാം. ഒരു സാധ്യത, ഫങ്ഷൻ എല്ലാ കോറുകളിലും പ്രവർത്തിപ്പിക്കാൻ ശ്രമിക്കുക എന്നതാണ്. അങ്ങനെയായാൽ നമുക്ക് കൂടുതൽ L2 ഞങ്ങളുടെ ഫങ്ഷന് ഉപയോഗിക്കാനാകും, അതുകൊണ്ട് അത് പരീക്ഷിക്കാം.

ആദ്യം, basel_chunks ഒരു N ശ്രേണി ഇൻപുട്ടായി എടുക്കാൻ മാറ്റാം.

# (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)

അടിസ്ഥാനപരമായി, പൈത്തണിന്റെ multiprocessing മൊഡ്യൂൾ pickles ഫങ്ഷൻ ഒബ്ജക്റ്റ്, കൂടാതെ 9 പുതിയ python3.10 ഇൻസ്റ്റൻസുകൾ സൃഷ്ടിക്കുന്നു, ഇവയെല്ലാം N=109, num_chunks = 50000 എന്നിവ ഉപയോഗിച്ച് ഫങ്ഷൻ എക്സിക്യൂട്ട് ചെയ്യുന്നു.

പ്രകടനം താരതമ്യം ചെയ്യാം.

ഫങ്ഷൻ 'basel_multicore' 1.156558 സെക്കൻഡിൽ എക്സിക്യൂട്ട് ചെയ്തു.
1.6449340658482263
ഫങ്ഷൻ 'basel_chunks' 1.027350 സെക്കൻഡിൽ എക്സിക്യൂട്ട് ചെയ്തു.
1.6449340658482263

അയ്യോ, ഇത് മോശമായി പ്രവർത്തിച്ചു. മൾട്ടിപ്രോസസിംഗ് ചെയ്യുന്ന പ്രാരംഭ പണി ചെലവേറിയതിനാലാവാം ഇത്, അതിനാൽ ഫങ്ഷൻ പൂർത്തിയാക്കാൻ തുലനാതീതമായി കൂടുതൽ സമയം എടുക്കുമ്പോൾ മാത്രമേ ഇത് ഗുണകരമാകൂ.

N 1010 ആക്കി വർദ്ധിപ്പിച്ചാൽ:

ഫങ്ഷൻ 'basel_multicore' 2.314828 സെക്കൻഡിൽ എക്സിക്യൂട്ട് ചെയ്തു.
1.6449340667482235
ഫങ്ഷൻ 'basel_chunks' 10.221904 സെക്കൻഡിൽ എക്സിക്യൂട്ട് ചെയ്തു.
1.6449340667482235

അതാ! ഇത് ≈5 മടങ്ങ് വേഗത്തിലാണ്. 1011 പരീക്ഷിക്കാം

ഫങ്ഷൻ 'basel_multicore' 13.844876 സെക്കൻഡിൽ എക്സിക്യൂട്ട് ചെയ്തു.
multicore 1.6449340668379773
ഫങ്ഷൻ 'basel_chunks' 102.480372 സെക്കൻഡിൽ എക്സിക്യൂട്ട് ചെയ്തു.
chunks 1.6449340668379773

അത് ≈8 മടങ്ങ് വേഗത്തിലാണ്! N വർദ്ധിക്കുമ്പോൾ, സിംഗിൾ-കോർ മുതൽ 10-കോർ വരെയുള്ള അനുപാതം 10:1 എന്നതിലേക്ക് നീണ്ടുകൂട്ടണം.

ചങ്ക് ചെയ്തതും മൾട്ടികോർ ഇംപ്ലിമെന്റേഷനുകളും തമ്മിലുള്ള റൺടൈം താരതമ്യം
ചങ്ക് ചെയ്തതും മൾട്ടികോർ ഇംപ്ലിമെന്റേഷനുകളും തമ്മിലുള്ള റൺടൈം താരതമ്യം

x-അക്ഷം ഒരു ലോഗ് സ്കെയിലിലാണ്, അതിനാലാണ് രേഖീയമായി വർദ്ധിക്കുന്ന റൺടൈമുകൾ എക്സ്പോണൻഷ്യലായി കാണപ്പെടുന്നത്

മൾട്ടിപ്രോസസിംഗ് ഉപയോഗിക്കുന്നതിന് ≈1 സെക്കൻഡ് ഓവർഹെഡ് ഉണ്ടെന്ന് നമുക്ക് കാണാം, അതിനാൽ ചങ്ക് ചെയ്ത റൺടൈം ഉയർന്നപ്പോൾ മാത്രമേ പ്രകടന ഗുണം ലഭിക്കൂ. ഇത് ≈N=1.3×109 എന്നതിൽ സംഭവിക്കുന്നു. എന്നിരുന്നാലും, അതിനുശേഷം, ഒരു വല്യ വ്യത്യാസമുണ്ട്.

പരീക്ഷണം വഴി (ഇവിടെ കാണിച്ചിട്ടില്ല) ഏകദേശം 50000 ആയ ഒരു chunk_size മൾട്ടിപ്രോസസിംഗ് കോഡിനും ഒപ്റ്റിമൽ ആണെന്ന് ഞാൻ കണ്ടെത്തി, ഒരു സിംഗിൾ കോറിനുള്ള L2 ഉപയോഗത്തിൽ ഓപറേറ്റിംഗ് സിസ്റ്റം ചില നിയന്ത്രണങ്ങൾ ഏർപ്പെടുത്തുന്നുവെന്ന് ഇത് നമ്മോട് പറയുന്നു.

ഫലങ്ങളുടെ സംഗ്രഹം

പൈത്തൺ ഫങ്ഷൻ N=108 (സെ) N=109 (സെ) N=1010 (സെ)
basel_multicore 1.04 1.17 2.33
basel_chunks 0.09 0.91 9.18
basel_np_broadcast 0.27 9.68 OOM
basel_less_pythonic 5.99 59.25 592 (ഏകദേ.)
basel (സഹജമായ) 7.64 68.28 682 (ഏകദേ.)
basel_np 0.618 44.27 OOM

കഴിഞ്ഞ പോസ്റ്റിലെ ഭാഷകളുമായി താരതമ്യം എങ്ങനെ?

ഭാഷ N=109 (മില്ലിസെക്കൻഡ്)
റസ്റ്റ് (rc, –release, മൾട്ടികോർ w/ rayon) 112.65
Python3.10 (basel_chunks) 913
റസ്റ്റ് (rc, –release) 937.94
സി (clang, -O3) 995.38
Python3.10 (basel_multicore) 1170
Python3.10 (basel_np_broadcast) 9680
ഹാസ്കെൽ (ghc, -O3) 13454
Python3.10 (basel_np) 44270
Python3.10 (basel_less_pythonic) 59250
Python3.10 (basel) 68280

വളരെ മികച്ചത്! ചങ്ക് ചെയ്ത നൂംപൈ കോഡാണ് ഏറ്റവും വേഗമുള്ള സീക്വൻഷ്യൽ പരിഹാരം! ഓർക്കുക, പൈത്തൺ കണക്കുകൂട്ടലുകൾ നടത്തുമ്പോൾ പശ്ചാത്തലത്തിൽ ഒരു ഇന്റർപ്രിറ്റർ മുഴുവനായി പ്രവർത്തിക്കുന്നു.

റസ്റ്റും പൈത്തൺ മൾട്ടികോറും തമ്മിലുള്ള താരതമ്യം:

ഭാഷ N=108 (മില്ലിസെ) N=109 (മില്ലിസെ) N=1010 (മില്ലിസെ) N=1011 (മില്ലിസെ)
റസ്റ്റ് 12.3 111.2 1083 10970
പൈത്തൺ 1040 1173 2330 12629

വ്യക്തമായും, ചെറിയ N മൂല്യങ്ങൾക്ക് പൈത്തണിന്റെ പ്രോസസ് അധിഷ്ഠിത സമാന്തരത്വത്തേക്കാൾ റസ്റ്റിന്റെ (ഓ.എസ്. ത്രെഡുകൾ വഴിയുള്ള) സമാന്തരീകരണ രീതി കൂടുതൽ കാര്യക്ഷമമാണ്. എന്നാൽ N കൂടുന്നതിനനുസരിച്ച് രണ്ടിനുമിടയിലുള്ള വ്യത്യാസം കുറയുന്നു.

ഉപസംഗ്രഹം

വ്യക്തമല്ലെങ്കിൽ, പൈത്തൺ ആന്തരികമായി എങ്ങനെ പ്രവർത്തിക്കുന്നു എന്ന് അറിയുന്നതിനായുള്ള ഒരു പരിശീലനമായിരുന്നു ഈ മുഴുവൻ ലേഖനവും. ദയവായി നിങ്ങളുടെ മാനേജറിനെ ശരിക്കും പ്രഭാവിതനാക്കാൻ നിങ്ങളുടെ പൈത്തൺ കോഡ് അതിമിതമായി ഒപ്റ്റിമൈസ് ചെയ്യാൻ ശ്രമിക്കരുത്. എല്ലായ്പ്പോഴും ഒരു മികച്ച പരിഹാരം ഉണ്ട്, ഇതുപോലെ:

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

←
ബാസൽ പ്രശ്നം (ഹലോ, ലോകം!)
ഇന്ററാക്ടീവ് MNIST എക്സ്പ്ലോറർ
→

back to top