This shows you the differences between two versions of the page.
|
asc:laboratoare:02 [2025/03/13 11:20] eduard.staniloiu [Resurse] |
asc:laboratoare:02 [2026/02/23 18:45] (current) giorgiana.vlasceanu |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laboratorul 02 - Fire de execuție în Python ====== | + | ====== Laboratorul 02 - Arhitecturi de Microprocesoare si Sisteme de Calcul ====== |
| ===== Obiective ===== | ===== Obiective ===== | ||
| - | Scopul acestui laborator îl reprezintă familiarizarea cu lucrul cu thread-uri și obiecte de sincronizare în Python. Pentru acestea aveți nevoie de cunoașterea elementelor de sintaxă Python prezentate în laboratorul 1 și de lucrul cu clase prezentat în laboratorul acesta. | + | Doua componente esentiale ale structurii unui sistem de calcul sunt reprezentate de catre procesor si placa de baza. Astfel, in acest laborator vom vorbi despre: |
| + | * Diferitele abordari ale structurii unui procesor | ||
| + | * Ce probleme si ce imbunatatiri ale performantei au motivat aparitia acestor structuri | ||
| + | * Arhitecturi CISC/RISC bazate pe procesoare Intel/AMD - inclusiv modele standard de placi de baza | ||
| + | * Comparatii intre abordarile curente utilizate in implementarea procesoarelor moderne si a placilor de baza | ||
| + | * Solutii de interconectare a procesoarelor AMD si Intel - Hypertransport si QuickPath Interconnect | ||
| - | ===== Recapitulare laborator 1 ===== | + | ===== Instruction level parallelism ===== |
| - | În cadrul laboratorului de ASC folosim versiunea 3.8 a Python-ului, suportul pentru versiunea 2.x terminându-se la data de 1 Ianuarie 2020. Această versiune este incompatibilă cu Python 2.x și unele construcții sau biblioteci este posibil să nu fie suportate nici de versiunile anterioare 3.8. | + | Masina Turing executa cate o instructiune la un moment dat. Cand programatorul scrie un program, ii este foarte simplu sa considere ca programul sau va fi executat in acest mod. Pe de alta parte, o masina care executa cate o instructiune este mai lenta decat una care executa mai multe instructiuni in paralel. Pentru a cumula avantajele celor doua abordari, ar trebui ca programatorul sa poata inca scrie cod ca pentru o masina seriala, iar procesorul sa execute acest cod cu un nivel de paralelism cat mai ridicat. Cum este posibil asa ceva? Cineva trebuie sa faca trecerea dintre perspectiva seriala a programatorului si perspectiva paralela pe care ne-ar placea sa o aiba procesorul. Acest cineva poate fi ori un compilator, ori un hardware specializat aflat in structura procesorului. |
| - | Particularități de limbaj: | + | Aceste considerente influenteaza structura procesorului si au dus la aparitia conceptului de Instruction Level Paralelism (ILP). Astfel, procesorul ia instructiuni dintr-un singur flux de control, le decodifica si executa in paralel. De exemplu, un procesor cu ILP poate sa scrie simultan rezultatele a doua instructiuni in registre, sa faca operatii aritmetice pentru alte trei, sa citeasca operanzii pentru alte doua, sa decodifice alte patru si sa ia (fetch) din fluxul de intrare inca patru instructiuni. |
| - | * **Indentarea** este obligatorie pentru a delimita blocurile de cod. | + | |
| - | * Este dynamically și strongly typed: | + | |
| - | * //dynamically typed// - pentru că tipurile variabilelor nu sunt precizate explicit în cod, și acestea se poate schimba pe măsură ce atribuim valori variabilelor | + | |
| - | * //strongly typed// - pentru că nu se pot face conversii de tip implicite (e.g. adunare de ''string'' cu ''int'') | + | |
| - | * pentru conversiile explicite între tipurile numerice, boolean și șiruri de caractere folosiți funcțiile [[https://docs.python.org/3.8/library/functions.html | built-in]] | + | |
| - | * Keywords: | + | |
| - | * ''None'' este echivalentul null în Java | + | |
| - | * ''pass'' este echivalentul unui bloc {} din c/java | + | |
| - | * Tipurile de date cele mai folosite sunt //int, float, string, boolean, list, tuple, dict//. | + | |
| - | Un fișier de cod Python este considerat un **modul**. Pentru a folosi alte module utilizăm ''import'' în următoarele modalități: | + | <note important> Cateva implementari de ILP includ: |
| - | <spoiler Click aici pentru exemple de import-uri > | + | * Pipeline: In acelasi ciclu de ceas, procesorul scrie rezultatul unei instructiuni in registre, executa operatia aritmetica a instructiunii urmatoare, si citeste operanzii instructiunii de dupa instructiunea urmatoare (la doua instructiuni dupa prima). |
| + | * VLIW (Very Long Instruction Word): Lanseaza mai multe instructiuni in acelasi ciclu de ceas. Compilatorul trebuie sa se asigure ca nu exista dependente de date intre acestea. La procesoarele superscalare, numarul de unitati de executie este transparent pentru setul de instructiuni. VLIW este insa constient de numarul de unitati de executie. | ||
| + | * Superscalar: Lanseaza mai multe instructiuni in acelasi ciclu de ceas. Dependenta de date este insa verificata de hardware aditional. Daca nu pot fi lansate in paralel, se va executa cate o instructiune secvential (neavand suport din partea compilatorului, exista si aceasta posibilitate). | ||
| + | * Planificare dinamica: Instructiunile sunt reordonate in timp ce sunt executate. In modul acesta, poate sa gaseasca usor instructiuni care nu au dependenta de date intre ele, pentru a fi executate simultan. | ||
| + | </note> | ||
| - | <code python import_example.py> | + | ILP-ul mareste asadar performanta procesorului. Dar de ce nu executam toate instructiunile deodata in paralel? Acesta ar fi de fapt modul cel mai rapid de a executa un program. Acest lucru nu se intampla deoarece ILP-ul are si anumite limitari, respectiv: |
| - | import random | + | * Dependenta de date: Daca rezultatul instructiunii A este operand pentru instructiunea B, atunci evident B nu poate fi executata inainte ca A sa se fi terminat. |
| - | random.randint(0,4) # trebuie specificat numele modulului | + | * Numar limitat de unitati functionale: Daca avem 5 sumatoare in procesor, nu putem executa mai mult de 5 sume simultan. |
| + | * Numar limitat de instructiuni lansate: Daca unitatea de lansare de instructiuni poate lansa maxim 5 instructiuni simultan, un program cu 500 de instructiuni va avea nevoie de 100 de operatii ale acestei unitati. | ||
| + | * Numar limitat de registre. | ||
| - | import random as rand # folosire alias pentru numele modulului | + | Mai multe detalii despre Explicitly Parallel Instruction Computing pot fi gasite aici [[:asc:extra:epic?480 | EPIC]]. |
| - | rand.randint(0,4) # trebuie specificat alias-ul | + | |
| - | from random import * # import tot continutul modulului | + | ===== Comparatie CISC vs. RISC ===== |
| - | randint(0,4) # nu mai trebuie specificat numele modulului | + | |
| - | from random import randint # import doar randint | + | Cand a aparut CISC, ideea era sa se aduca in hardware stilul de programare specific unui limbaj care sa se aproprie (pe cat e posibil la nivelul hardware) de un limbaj cat mai inalt. Astfel, instructiunile complexe au acelasi efect ca micile secvente de instructiuni simple. Implemenarea acestor instructiuni complexe in hardware insemana insa: |
| - | randint(0,4) | + | |
| - | </code> | + | |
| - | </spoiler> | + | |
| + | <note important> | ||
| + | * Hardware complex | ||
| + | * Locul ocupat de hard-ul pentru instructiunile complexe ar fi putut fi utilizat pentru a avea mai multe unitati de executie (si deci grad de paralelism mai mare) | ||
| + | * Secvente de microcod, care sunt lente comparativ cu restul procesorului | ||
| + | </note> | ||
| - | **Funcțiile** se declară folosind keyword-ul ''def'' și nu li se specifică tip de return sau tipuri pentru parametri. Se poate simula supraîncărcarea (overloading) metodelor folosind parametri cu valori implicite. Funcțiile sunt de fapt obiecte. | + | Datorita setului redus de instructiuni de asamblare, compilatoarele optimizate pentru RISC sunt capabile sa organizeze mai eficient fluxul de instructiuni de asamblare. Pe de alta parte insa, compilatoarele optimizate pentru RISC necesita mai mult timp de compilare decat cele pentru CISC. Aceasta deoarece trebuie sa se ocupe si de management-ul benzii de asamblare, anticiparea ramificatiilor (branch prediction) sau reorganizarea codului. |
| - | <spoiler Click aici pentru exemple de funcții > | + | Ca principiu, o arhitectura RISC are mai multe registre generale, in timp ce CISC are mai multe registre speciale. Practic toate procesoarele moderne imprumuta atat caracteristici CISC cat si RISC. |
| - | <code python func_example.py> | + | Exista trei tipuri de categorii de instructiuni CISC, si anume: |
| - | def f(a, b="world", c=0): | + | * Aritmetico-logice |
| - | print (" ".join([a, b, str(c)])) | + | * De control secvential |
| + | * De acces la memorie | ||
| - | f("hello") # hello world 0 | + | Formatul instructiunilor RISC are o lungime fixa, cu lungimea unei instructiuni in general egala cu lungimea cuvantului de memorie; in cazul CISC, lungimea unei instructiuni variaza in functie de formatul instructiunii. RISC are un numar mic de moduri de adresare, spre deosebire de CISC, care are un numar mare de moduri de adresare (utilizate mai rar). |
| - | f("hello", "lab") # hello lab 0 | + | |
| - | f("hello", "lab", 2) # hello lab 2 | + | |
| - | f("hello", c=2) # hello world 2 | + | |
| - | f("hello", "lab", c=2) # hello lab 2 | + | |
| - | </code> | + | Setul de instructiuni RISC este orientat pe registre (peste 32 de registre). Pentru ca accesul la memorie este mult mai lent decat lucrul cu registrele, RISC incurajeaza lucrul cu acestia. Face acest lucru prin cresterea numarului de registre si prin limitarea explicita a acceselor la memorie. In general instructiunile au 2 operanzi (registre) si un registru destinatie. |
| - | </spoiler> | + | <note important> |
| + | In cadrul arhitecturilor RISC exista o limitare explicita, si anume: singurul mod de acces la memorie este prin load si store. Aceasta se deosebeste fundamental de CISC care are instructiuni cu operanzi locatii de memorie. Totusi, desi RISC impune aceasta disciplina de lucru cu memoria, doar 20-25% din codul unui program e reprezentat de operatii de tip load sau store. | ||
| + | </note> | ||
| - | Construcția **''<nowiki>if __name__ == "__main__"</nowiki>''** delimitează 'main'-ul unui modul. Aceasta nu este obligatorie, însă dacă nu e folosită, orice cod cu indentare top level s-ar executa de fiecare dată când fișierul este parsat (ex: când este importat). | + | Fie urmatorul exemplu de cod in C: |
| + | <code C> | ||
| + | #include <stdio.h> | ||
| + | #define N 100 | ||
| - | ===== Ce este un thread? ===== | + | int main() { |
| - | Sistemele de calcul moderne sunt capabile de a executa mai multe operații în acelasi timp. Sistemul de operare este cel care permite rularea mai multor aplicații simultan, dar această idee se poate extinde și la nivelul unei aplicații. De exemplu, o aplicație ce rulează un stream video online trebuie simultan să //citească// conținutul video de pe rețea, să îl //decomprime//, să //actualizeze// display-ul local cu aceste informații etc. Spunem că aplicațiile ce oferă aceste capabilități constituie un software concurent. | + | int a[N], i; |
| + | for(i=0;i<N;i++) | ||
| + | a[i] = 2*i; | ||
| + | return 0; | ||
| + | } | ||
| - | //Deci ce este concurența?// **Concurența** este proprietatea unei logici de program de a putea executa **simultan** un set de task-uri. **Paralelismul** reprezintă o metodă de implementare a acestei paradigme de programare ce permite rularea unui set de task-uri într-un mod care utilizează core-uri multiple, procesoare multiple sau chiar mai multe mașini (într-o structură de tip cluster de exemplu). | + | </code> |
| - | Thread-urile reprezintă o metodă de implementare a concurenței, fiind fire de execuție create (//spawned//) în cadrul unui program principal (//process//) ce execută concurent task-uri definite de programator. Un fir de execuție este parte a unui proces, iar implementarea diferă de la un sistem de operare la altul. Mai multe thread-uri pot exista în cadrul aceluiași proces, ele partajând anumite resurse: memorie, descriptori I/O etc. În această privință thread-urile diferă de procese prin faptul că variabilele globale pot fi accesate de către toate thread-urile unui proces și pot servi ca mediu de comunicație între thread-uri. Fiecare thread are totuși și un set propriu de variabile locale. Din acest motiv thread-urile mai sunt numite și //lightweight processes//. | + | Rezultatul compilarii codului pana la nivel de asamblare, pe arhitectura CISC si RISC este urmatorul: |
| + | <spoiler Arata codul> | ||
| + | * RISC: | ||
| + | <code asm> | ||
| + | root@raspberrypi:~# cat risc.s | ||
| + | .arch armv6 | ||
| + | .eabi_attribute 28, 1 | ||
| + | .eabi_attribute 20, 1 | ||
| + | .eabi_attribute 21, 1 | ||
| + | .eabi_attribute 23, 3 | ||
| + | .eabi_attribute 24, 1 | ||
| + | .eabi_attribute 25, 1 | ||
| + | .eabi_attribute 26, 2 | ||
| + | .eabi_attribute 30, 6 | ||
| + | .eabi_attribute 34, 1 | ||
| + | .eabi_attribute 18, 4 | ||
| + | .file "risc.c" | ||
| + | .text | ||
| + | .align 2 | ||
| + | .global main | ||
| + | .arch armv6 | ||
| + | .syntax unified | ||
| + | .arm | ||
| + | .fpu vfp | ||
| + | .type main, %function | ||
| + | main: | ||
| + | @ args = 0, pretend = 0, frame = 408 | ||
| + | @ frame_needed = 1, uses_anonymous_args = 0 | ||
| + | @ link register save eliminated. | ||
| + | str fp, [sp, #-4]! | ||
| + | add fp, sp, #0 | ||
| + | sub sp, sp, #412 | ||
| + | mov r3, #0 | ||
| + | str r3, [fp, #-8] | ||
| + | b .L2 | ||
| + | .L3: | ||
| + | ldr r3, [fp, #-8] | ||
| + | lsl r2, r3, #1 | ||
| + | ldr r3, [fp, #-8] | ||
| + | lsl r3, r3, #2 | ||
| + | sub r1, fp, #4 | ||
| + | add r3, r1, r3 | ||
| + | str r2, [r3, #-404] | ||
| + | ldr r3, [fp, #-8] | ||
| + | add r3, r3, #1 | ||
| + | str r3, [fp, #-8] | ||
| + | .L2: | ||
| + | ldr r3, [fp, #-8] | ||
| + | cmp r3, #99 | ||
| + | ble .L3 | ||
| + | mov r3, #0 | ||
| + | mov r0, r3 | ||
| + | add sp, fp, #0 | ||
| + | @ sp needed | ||
| + | ldr fp, [sp], #4 | ||
| + | bx lr | ||
| + | .size main, .-main | ||
| + | .ident "GCC: (Raspbian 8.3.0-6+rpi1) 8.3.0" | ||
| + | .section .note.GNU-stack,"",%progbits | ||
| + | </code> | ||
| - | + | * CISC: | |
| - | + | <code asm> | |
| - | ===== Clase și obiecte în Python ===== | + | [razvan.dobre@fep7-1 ~]$ cat cisc.s |
| - | + | .file "cisc.c" | |
| - | Trebuie subliniat că în Python, cuvântul "obiect" nu se referă neapărat la instanța unei clase. [[https://docs.python.org/3.8/tutorial/classes.html | Clasele]] în sine sunt obiecte, iar, în sens mai larg, în Python toate tipurile de date sunt obiecte. Există tipuri de date care nu sunt clase: numerele întregi, listele, fișierele. | + | .text |
| - | + | .globl main | |
| - | O clasă, în sensul C++/Java, se crează în Python prin folosirea cuvantului cheie ''class''. Exemplul de mai jos creează un obiect de tip //class// cu numele //ClassName//. Acesta este echivalent cu o clasă C++/Java numită //ClassName//. //Interiorul clasei// poate conține definiții de metode sau clase și atribuiri de variabile. Clasa este derivată din //SuperClass1// și din //SuperClass2//. Spre deosebire de Java, numele fișierului sursă nu trebuie să fie la fel cu al vreunei clase definite în el. | + | .type main, @function |
| - | + | main: | |
| - | <code python> | + | .LFB0: |
| - | class ClassName (SuperClass1, SuperClass2): | + | .cfi_startproc |
| - | [interiorul clasei] | + | pushq %rbp |
| + | .cfi_def_cfa_offset 16 | ||
| + | .cfi_offset 6, -16 | ||
| + | movq %rsp, %rbp | ||
| + | .cfi_def_cfa_register 6 | ||
| + | subq $296, %rsp | ||
| + | movl $0, -4(%rbp) | ||
| + | jmp .L2 | ||
| + | .L3: | ||
| + | movl -4(%rbp), %eax | ||
| + | leal (%rax,%rax), %edx | ||
| + | movl -4(%rbp), %eax | ||
| + | cltq | ||
| + | movl %edx, -416(%rbp,%rax,4) | ||
| + | addl $1, -4(%rbp) | ||
| + | .L2: | ||
| + | cmpl $99, -4(%rbp) | ||
| + | jle .L3 | ||
| + | movl $0, %eax | ||
| + | leave | ||
| + | .cfi_def_cfa 7, 8 | ||
| + | ret | ||
| + | .cfi_endproc | ||
| + | .LFE0: | ||
| + | .size main, .-main | ||
| + | .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)" | ||
| + | .section .note.GNU-stack,"",@progbits | ||
| </code> | </code> | ||
| + | </spoiler> | ||
| + | ===== Arhitectura Intel ===== | ||
| - | Clasele suportă **multiple inheritence** și nu există un contract propriu-zis pentru interfețe. Pentru a crea clase abstracte există modulul [[http://docs.python.org/library/abc.html | abc (Abstract Base Classes)]]. Pentru metodele pe care vreți să le considerați abstracte puteți transmite excepția ''NotImplementedError'' sau puteți adăuga în corpul funcției doar keyword-ul ''pass''. Pentru a compara conceptele de programare orientată pe obiect în Python cu ceea ce sunteți familiari din Java vă recomandăm linkul [[https://realpython.com/oop-in-python-vs-java/ | OOP in Python vs Java]]. | + | O schema clasica pentru un sistem CISC este prezentata in Figura 1. Aici se poate distinge usor in partea de sus Procesorul, legat de restul sistemului prin Front-Side-Bus (FSB) de 400/533/800MHz catre North Bridge (i.e. 82865PE MCH). Pe North Bridge se afla controllerul de memorie, si ca atare si memoriile sunt conectate direct aici prin canale intre 2.1GB/s si 3.2GB/s. De asemenea pe North Bridge se conecteaza atat placa grafica (Accelerated Graphics Port - AGP 8x/4x), cat si interfata de retea de mare viteza Gigabit Ethernet. |
| + | |||
| + | {{ :asc:lab4:865pe.jpg?640 |Figura 1. Schema bloc a Chipsetului Intel® 865PE }} | ||
| + | La randul sau North Bridge-ul este conectat printr-o legatura de 266MB/s catre South Bridge (i.e. 82801EB ICH5 / 82801ER ICH5R). Dupa cum se poate vedea, North Bridge-ul impreuna cu South Bridge-ul formeaza impreuna ceea ce se numeste Chipsetul Intel® 865PE. Urmarind in continuare schema din Figura 1, se observa ca pe South Bridge se conecteaza o multitudine de componente periferice cu o viteza si rata de transfer de date considerabil mai scazuta decat elementele conectate pe North Bridge, cum ar fi: AC97 (placa audio), porturi ATA si Serial ATA, porturi USB, sistem de management si de control al consumului, etc. | ||
| - | În lucrul cu clase, trebuie avute în vedere următoarele reguli: | + | Din aceasta schema se poate usor deduce ca punctul vulnerabil al acestor sisteme il constituie integrarea controllerului de memorie pe North Bridge, si in special legaturile de marime limitata intre South si North Bridge, precum si intre North Bridge si procesor. In mod evident, dimensionarea acestora este un compromis de design al sistemelor Intel, menit sa deserveasca majoritatea sistemelor hardware bazate pe acest Chipset, si a aplicatiilor ce ruleaza pe ele. |
| - | * Primul argument pentru metodele unei clase este întotdeauna obiectul sursă, numit **''self''**, echivalent-ul lui ''this''. | + | |
| - | * Când ne referim la membrii clasei, trebuie să folosim ''self.membru'', într-un mod asemanator cu folosirea "this" din Java (doar că în Python este obligatoriu să folosim ''self'' nu doar pentru a face distincție între câmpurile clasei și parametrii/variabilele cu aceleași nume din funcții). | + | |
| - | * Metoda specială ''<nowiki>__init__()</nowiki>'' este apelată la instanțierea clasei și poate fi considerată un **constructor**. Definirea metodelor ''<nowiki>__init__()</nowiki>'' este opțională. | + | |
| - | * Metoda specială ''<nowiki>__del__()</nowiki>'' este apelată când nu mai sunt referințe la acest obiect și poate fi asemuită cu un **destructor**. Definirea metodelor ''<nowiki>__del__()</nowiki>'' este opțională. | + | |
| - | * În cazul moștenirii, în metoda ''<nowiki>__init__()</nowiki>'' trebuie întâi apelat ''<nowiki>__init__()</nowiki>''-ul claselor părinte. | + | |
| - | * Implicit toate câmpurile si metodele claselor sunt publice. Pentru a declara un câmp/metodă privată numele acesteia trebuie prefixat cu ''<nowiki>__</nowiki>'' (mai multe detalii puteți afla [[https://www.bogotobogo.com/python/python_private_attributes_methods.php | aici]]). | + | |
| - | * Instanțierea se face prin apelarea obiectului clasă, posibil cu argumente. | + | |
| - | <code python class_example.py> | + | Pentru a atinge insa performante mai inalte, mai ales in contextul aparitiei sistemelor multi-procesor si multi-core, este insa nevoie de imbunatatiri ale acestei abordari, cum se poate observa la Chipsetul Intel 7300 din Figura 2. |
| - | class Student: | + | |
| - | """ O clasa care reprezinta un student. Comentariile docstring se pun dupa declaratie :) """ | + | {{ :asc:lab4:intel7300chipset.jpg?640 |Figura 2. Schema bloc a Chipsetului Intel® 7300}} |
| - | def __init__(self, name, grade=5): # constructor cu parametru default; echivalent cu mai multi constructori overloaded | + | Aici se poate observa conectarea a patru procesoare catre North Bridge (MCH - Memory Controller Hub) prin canale distincte FSB, de 1066MHz fiecare, menite sa asigure o alimentare eficienta cu date a acestora. Controllerul de memorie ramane pe North Bridge insa memoria se conecteaza prin patru canale de 8GB/s. Apar pe North Bridge conexiuni multiple PCI-Express (PCI-E), placa de retea devine doar "una dintre acestea", iar latimea de banda este crescuta pentru fiecare dintre aceste componente fata de versiunile anterioare de Chipseturi. |
| - | self.name = name # campurile clasei pot fi declarate oriunde! | + | |
| - | self.change_grade(grade) # apelul unei metode a clasei | + | |
| - | def change_grade(self, grade): # primul parametru este intotdeauna 'self' | + | Pentru simplitate, si conexiunea intre North Bridge si South Bridge (631xESB I/O Controller) este realizata prin conexiuni PCI Express 2x sau chiar 4x. Este sugestiv de asemenea faptul ca South Bridge-ul poarta acum numele de "I/O Controller" si se consfinteste astfel si prin nume direct rolul South Bridge-ului. Porturile ATA sunt inlocuite acum de SATA si PATA, apar din nou numeroase porturi PCI-E si PCI-X, USB, de gestiune a consumului, a biosului sau placi aditionale de retea pentru management. |
| - | self.grade = grade # adauga nou camp clasei | + | |
| - | x = Student("Alice") | + | Din prezentarea celor doua chipseturi 865PE si 7300 se poate vedea evolutia arhitecturala din jurul procesoarelor CISC de la Intel, cu o modularitate si o flexibilitate considerabil mai mare a chipsetului 7300, care ofera un potential de performanta mult crescut pentru sistemele ce il utilizeaza. Atentie, atat procesoarele Intel considerate pentru Chipset-urile prezentate, cat si cele AMD din sectiunile urmatoare, sunt din familii dedicate sistemelor de inalta performanta si cele mai puternice din clasa lor. Discutia prezentata este insa relevanta, la o scara corespunzator mai scazuta, si pentru celelalte sisteme si procesoare oferite de cele doua mari firme. |
| - | y = Student("Bob", 10) | + | |
| - | x.change_grade(8) | + | |
| - | </code> | + | |
| + | Anexa: | ||
| + | Magistrale uzuale si latimea lor de banda maxima | ||
| - | Clasele Python pot avea membri statici. În cazul câmpurilor, ele sunt declarate în afara oricărei metode a clasei. Pentru metode avem două variante: una folosind [[https://realpython.com/primer-on-python-decorators/#decorating-classes|decoratorul]] @staticmethod, cealaltă folosind funcția built-in [[https://docs.python.org/3.8/library/functions.html#staticmethod|staticmethod]]. Observați că metodele statice nu au parametrul ''self''. | + | ^ Bus ^ Max Bandwidth ^ |
| - | <code python static_example.py> | + | |PCI|132 MB/s| |
| - | class Util: | + | |AGP 8X|2,100 MB/s| |
| - | x = 2 # camp static | + | |PCI Express 1x|250 [500]* MB/s| |
| - | + | |PCI Express 2x|500 [1000]* MB/s| | |
| - | @staticmethod # metoda statica | + | |PCI Express 4x|1000 [2000]* MB/s| |
| - | def do_stuff(): | + | |PCI Express 8x|2000 [4000]* MB/s| |
| - | print ("stuff") | + | |PCI Express 16x|4000 [8000]* MB/s| |
| - | + | |PCI Express 32x|8000 [16000]* MB/s| | |
| - | def do_otherstuff(): # alta varianta de a declara o metoda statica | + | |IDE (ATA 100)|100 MB/s| |
| - | print ("other stuff") | + | |IDE (ATA 133)|133 MB/s| |
| - | do_otherstuff = staticmethod(do_otherstuff) | + | |SATA|150 MB/s| |
| - | + | |Gigabit Ethernet|125 MB/s| | |
| - | print (Util.x) | + | |IEEE 1394B [Firewire]|100 MB/s| |
| - | Util.do_stuff() | + | |
| - | Util.do_otherstuff() | + | |
| - | </code> | + | |
| <note tip> | <note tip> | ||
| - | //Clase Python pe scurt:// | + | PCI Express este o magistrala seriala (datele pot circula simultan in ambele directii). In tabelul de mai sus cele doua valori pentru latimea de banda corespund latimii de banda intr-o singura directie, respectiv in ambele directii (combinat). |
| - | * trebuie să folosiți ''self.nume_funcție'' sau ''self.nume_variabila'' | + | |
| - | * o clasă poate moșteni mai multe clase | + | |
| - | * ''<nowiki>__init__()</nowiki>'' este numele constructorului. Puteți avea un singur constructor, pentru a simula mai mulți contructori folosiți parametri default. | + | |
| - | * puteți avea metode și variabile statice | + | |
| - | * nu aveți //access modifiers// | + | |
| - | * instanțiere: ''nume_instanta = NumeClasa(argumente_constructor)'' | + | |
| </note> | </note> | ||
| - | ===== Programare concurentă în Python ===== | + | ===== Arhitectura AMD Hammer ===== |
| - | În Python, programarea concurentă este facilitată de modulul [[http://docs.python.org/3/library/threading.html | threading]]. Acest modul oferă clasa [[http://docs.python.org/3/library/threading.html#thread-objects | Thread]], care permite crearea și managementul thread-urilor, precum și o serie de clase ([[http://docs.python.org/3/library/threading.html#condition-objects |Condition]], [[http://docs.python.org/3/library/threading.html#event-objects | Event]], [[http://docs.python.org/3/library/threading.html#lock-objects | Lock]], [[http://docs.python.org/3/library/threading.html#rlock-objects | RLock]], [[http://docs.python.org/3/library/threading.html#semaphore-objects | Semaphore]], [[https://docs.python.org/3/library/threading.html#barrier-objects|Barrier]]) care oferă modalități de sincronizare și comunicare între thread-urile unui program Python. | + | Din familia Hammer, sau AMD64, face parte cel mai puternic procesor de la AMD, si anume Opteron. Opteronul este echivalentul familiilor Intel Itanium si Intel Xeon, destinat serverelor si sitemelor de inalta performanta. Opteron este un procesor out-of-order si in interiorul unitatii de executie ordinea instructiunilor este schimbata, pentru a maximiza eficienta. Pentru utilizatorul extern insa, instructiunile par a se executa in aceeasi ordine in care au fost lansate. De asemenea, el este 3-way superscalar, adica poate decoda, executa si incheia trei instructiuni x86 la fiecare ciclu masina. Desi poate lucra in paralel la 3 instructiuni, aceasta nu insemna neaparat ca cele 3 instructiuni sunt procesate in intregime pe acea perioda de ceas. Opteronul a fost creat pentru a putea lucra in sisteme multiprocesor si fiind primul care a oferit o scalabilitate sporita, el a acaparat la vremea respectiva o portiune semnificativa din piata comerciala de servere. |
| - | ==== Thread-uri ==== | + | Printre cele mai importante imbunatatiri arhitecturale cu care vine Hammer (generatia 8) fata de Athlon si AthlonXP (generatia 7) se numara: doua stagii in plus la pipeline, algoritmi imbunatatiti de predictie a ramificatiilor, suport pentru SSE2 (Streaming SIMD Extension 2 - Streaming Multimedia Instructions), controller de memorie integrat in CPU si extensie completa pentru setul de instructiuni pe 64 biti pentru x86. Toate procesoarele Hammer au viteze de maxim 2.5GHz. Pe de alta parte, Intel s-a concentrat mai mult pe cresterea vitezei, fara a se preocupa excesiv de paralelism, acest trend a fost insa oprit din 2007-2008, cand si Intel, si AMD au trecut la producerea de sisteme multi-core. Acest lucru este important in lumea serverelor, unde disiparea caldurii este o problema principala. Intel insa a venit si cu o noua "arma" si anume tehnologia de integrare bazata pe dielectrici High-K, ce a dus la o scadere drastica a consumului, si a permis de asemenea performante considerabile la frecvente de executie scazute. |
| - | Un fir de execuție concurentă este reprezentat în Pyhton de clasa //Thread//. Cel mai simplu mod de a specifica instrucțiunile care se doresc a fi rulate concurent, este de a apela constructorul lui //Thread// cu numele unei funcții care conține aceste instrucțiuni, precum în exemplul următor. Pornirea thread-ului se face apoi cu metoda //start()//, iar pentru a aștepta terminarea execuției thread-ului se folosește metoda //join()//. | + | {{ :asc:lab4:amdquadcorephenom.jpg?640 |Figura 3. Arhitectura Procesorului Quad-Core Phenom}} |
| - | <code python task01.py> | + | Dupa cum am spus, Hammer pastreaza compatibilitatea cu 16 si 32 biti (fata de Intel, care renunta complet la x86 si trece la IA-64). Pentru a putea face acest lucru, Hammer are doua moduri de operare: Legacy- si Long-Mode. Long Mode este subdivizat si el in Compatibility mode si 64bit Mode. Legacy mode este destinat exclusiv sistemelor de operare pe 16 si 32 biti. Compatibility mode este destinat sistemelor de operare pe 64 de biti, dar care ruleaza programe scrise pentru 32 biti. Astfel, desi programul in sine nu beneficiaza de facilitatile 64 biti, managamentul resurselor, facut de sistemul de operare pe 64 de biti beneficiaza de totate avantajele date de rularea pe 64 biti. |
| - | from threading import Thread | + | |
| - | + | ||
| - | def my_concurrent_code(nr, msg): | + | |
| - | """ Functie care va fi rulata concurent """ | + | |
| - | print ("Thread", nr, "says:", msg) | + | |
| - | # creeaza obiectele corespunzatoare thread-urilor | + | {{ :asc:lab4:amdquadcorephenomcache.jpg?640 |Figura 4. Arhitectura Cache-ului la Phenom}} |
| - | t1 = Thread(target = my_concurrent_code, args = (1, "hello from thread")) | + | |
| - | t2 = Thread(target = my_concurrent_code, args = (2, "hello from other thread")) | + | |
| - | # porneste thread-urile | + | {{ :asc:lab4:corex64.jpg?480 |Figura 5. Schema Bloc a unui Core AMD x64}} |
| - | t1.start() | + | |
| - | t2.start() | + | |
| - | # executia thread-ului principal continua de asemenea | + | ==== Controller-ul de memorie integrat in procesor la AMD ==== |
| - | print ("Main thread says: hello from main") | + | |
| - | # asteapta terminarea thread-urilor | + | In general, un procesor foloseste doua cipuri de pe placa de baza pentru a accesa memoria si perifericele. Aceste sunt numite North Bridge si South Bridge, dupa cum au fost descrise in sectiunile anterioare. Se observa ca North Bridge-ul joaca un rol esential, el facand legatura cu memoria. De acea, de la generatia Hammer, AMD a integrat in cipul procesorului North Bridge-ul. In felul acesta se obtine o latenta de acces la memorie redusa cu cel putin 20%. Acest controller are o legatura de 128 biti cu memoria. In plus, el functioneaza dupa ceasul procesorului, acest lucru marind inca o data viteaza; permite de asemenea ca marirea frecventei de ceas a procesorului sa imbunatateasca si performantele controller-ului. |
| - | t1.join() | + | |
| - | t2.join() | + | |
| - | </code> | + | |
| - | Se folosește parametrul //target// al constructorului pentru a pasa numele funcției concurente și, opțional, pot fi folosiți parametrii //args// sau //kwargs// pentru a specifica argumentele funcției concurente, dacă ele există. //args// este folosit pentru a trimite argumentele funcției concurente ca un tuplu, iar //kwargs// este folosit pentru a trimite argumentele ca un dicționar. | + | Controller-ul de memorie are grija si de coerenta cacheului. Cipul integrat se concentreaza acum doar pe comunicarea cu memoria. Alte functionalitati ale North Bridge-ului, cum erau de exemplu comunicarea cu AGP sau Placa de Retea, au fost mutate pe un cip extern. |
| - | Pentru a diferenția un tuplu cu un singur element de folosirea obișnuită a parantezelor se utilizează următoarea sintaxă: | + | |
| - | <code python> | + | |
| - | # t contine int-ul 42 | + | |
| - | t = (42) | + | |
| - | # t contine un tuplu cu un singur element | + | |
| - | t = (42,) | + | |
| - | </code> | + | |
| + | {{ :asc:lab4:opteronmem.jpg?640 |Figura 6. Controller de memorie integrat de la procesoarele Opteron}} | ||
| - | <note important> | + | ==== Hypertransport ==== |
| - | Crearea unui obiect Thread nu pornește execuția thread-ului. Acest lucru se întâmplă doar după apelul metodei **//start()//**. | + | |
| - | </note> | + | |
| - | O metodă alternativă de a specifica instrucțiunile care se doresc a fi rulate concurent este de a crea o subclasă a lui //Thread// care suprascrie metoda //run()//. Se poate de asemenea suprascrie și metoda //<nowiki>__init__()</nowiki>// (constructorul) pentru a primi argumentele cu care vor fi inițializate câmpurile proprii subclasei. Dacă optați pentru această abordare nu este indicat să suprascrieți alte metode ale clasei //Thread//, decât constructorul și //run()//. | + | Hypertransport este o tehnologie pentru I/O dezvoltata initial de AMD. Ea este o alternativa la sistemele actuale de bus. Foloseste legaturi duble, punct la punct, pentru a lega componentele intre ele. Este, in termeni de retele, echivalentul unei legaturi full-duplex punct la punct fata de o topologie bus. |
| - | <code python task02.py> | + | <note tip> |
| - | from threading import Thread | + | O astfel de lagatura poate avea intre 4 si 64 biti, si poate opera la viteze de 400Mhz-2.6GHz. Datele sunt impachetate si trimise folosind un protocol care prevede trimiterea de pachete multiplu de 4 bytes, cu marimi intre 4 si 64 bytes. |
| - | + | ||
| - | class MyThread(Thread): | + | |
| - | """ Clasa care incapsuleaza codul nostru concurent """ | + | |
| - | def __init__(self, nr, msg): | + | |
| - | Thread.__init__(self) | + | |
| - | self.nr = nr | + | |
| - | self.msg = msg | + | |
| - | + | ||
| - | def run(self): | + | |
| - | print ("Thread", self.nr, "says:", self.msg) | + | |
| - | + | ||
| - | # creeaza obiectele corespunzatoare thread-urilor | + | |
| - | t1 = MyThread(1, "hello from thread") | + | |
| - | t2 = MyThread(2, "hello from other thread") | + | |
| - | + | ||
| - | # porneste thread-urile | + | |
| - | t1.start() | + | |
| - | t2.start() | + | |
| - | + | ||
| - | # executia thread-ului principal continua de asemenea | + | |
| - | print ("Main thread says: hello from main") | + | |
| - | + | ||
| - | # asteapta terminarea thread-urilor | + | |
| - | t1.join() | + | |
| - | t2.join() | + | |
| - | + | ||
| - | </code> | + | |
| - | + | ||
| - | + | ||
| - | + | ||
| - | <note important> | + | |
| - | La suprascrierea constructorului clasei //Thread// nu uitați să apelați și constructorul clasei de bază. | + | |
| </note> | </note> | ||
| - | |||
| <note tip> | <note tip> | ||
| - | Interpretorul cel mai popular de Python (CPython) folosește un lock intern (GIL - Global Interpreter Lock) pentru a simplifica implementarea unor operații de nivel scăzut (managementul memoriei, apelul extensiilor scrise în C etc.). Acest lock permite execuția unui singur thread în interpretor la un moment dat și limitează paralelismul și performanța thread-urilor Python. Mai multe detalii despre GIL puteți găsi în această [[http://www.dabeaz.com/python/UnderstandingGIL.pdf | prezentare]]. | + | Hypertransport e compatibil cu PCI, de aceea a fost usor de introdus. El poate lucra in doua moduri: coerent si non-coerent. Modul coerent e folosit pentru comunicatiile interprocesor. Modul non-coerent e optimizat pentru comunicatiile I/O. |
| </note> | </note> | ||
| + | ==== Integrarea in Arhitectura Hammer ==== | ||
| - | Pe lângă clasa //Thread// și clasele de sincronizare, modulul [[https://docs.python.org/3/library/threading.html|threading]] mai conține și o serie de funcții ce ofera informații despre threadurile active. Python 3 a adăugat pe parcursul versiunilor sale noi astfel de metode utile. | + | E folosit pentru a lega controller-ul de memorie integrat (fostul NorthBridge) de memorie. In mod similar este folosit in sistemele multiprocesor pentru comunicarea interprocesor, folosind modul coerent. |
| - | Observați că spre deosebire de Java, acestea sunt funcții definite în modul, nu apelate din cadrul vreunei clase. | + | |
| - | <code Python> | + | AMDOpteron are 3 legaturi Hypertransport. Seria 100 are 3 legaturi non-coerente, deoarece, fiind destianta monoprocesoareleor, nu are nevoie de comunicatie interprocesor. Seria 200 are 2 linii non-coerente si una coerenta, pentru unica legatura dintre cele doua procesoare (seria 200 e pentru dual-procesor). Si seria 300 are toate cele 3 legaturi coerente. |
| - | >>> import threading | + | |
| - | >>> threading.current_thread() | + | |
| - | <_MainThread(MainThread, started 4566906304)> | + | |
| - | >>> threading.active_count() | + | |
| - | 1 | + | |
| - | >>> threading.enumerate() | + | |
| - | [<_MainThread(MainThread, started 4566906304)>] | + | |
| - | >>> # Functii adaugate in Python 3 | + | |
| - | >>> threading.get_ident() | + | |
| - | 4566906304 | + | |
| - | >>> threading.get_native_id() # identificator folosit de kernel | + | |
| - | 3137981 | + | |
| - | >>> threading.main_thread() | + | |
| - | <_MainThread(MainThread, started 4566906304)> | + | |
| - | >>> # One-liner pentru crearea si pornirea unui thread | + | |
| - | >>> threading.Thread(target=lambda a: print("ASC, lab %d", a), args=([2])).start() | + | |
| - | ASC, lab %d 2 | + | |
| - | </code> | + | |
| - | ==== Elemente de sincronizare ==== | + | {{ :asc:lab4:hypertransport.jpg?640 |Figura. 7 Hypertransport intern si extern}} |
| - | Pentru ca un program concurent să funcționeze corect este nevoie ca firele sale de execuție să coopereze în momentul în care vor să acceseze date partajate. Această cooperare se face prin intermediul partajării unor elemente de sincronizare care pun la dispoziție un API ce oferă anumite garanții despre starea de execuție a thread-urilor care le folosesc. | + | <note tip> |
| - | + | Liniile sunt de 16 biti, bidirectionale cu frecvente intre 200Mhz si 800Mhz, de aici rezultand o viteza de 6.4 Gbytes/sec (3.2 Gbytes/sec in fiecare directie). Cum Opteron are 3 astfel de lagaturi, poate comunica, deci, cu 19.2 Gbytes/sec. | |
| - | === Thread === | + | |
| - | + | ||
| - | Pe lângă facilitățile de creare a noi fire de execuție, obiectele de tip //Thread// reprezintă și cele mai simple elemente de sincronizare, prin intermediul metodelor //start()// și //join()//. | + | |
| - | + | ||
| - | Metoda //start()// garantează că toate rezultatele thread-ului care o apelează (să-l numim //t1//), până în punctul apelului, sunt disponibile și în thread-ul care va porni (să-l numim //t2//). A se observa că nu se oferă nici un fel de garanție despre rezultatele lui //t1// care urmează după apel. //t2// nu poate face nici o presupunere în acest caz, fără a folosi alte elemente de sincronizare. | + | |
| - | + | ||
| - | Metoda //join()// garantează thread-ului care o apelează (să-l numim //t1//) că thread-ul asupra căreia este apelată (să-l numim //t2//) s-a terminat și nu mai accesează date partajate. În plus toate rezultatele lui //t2// sunt disponibile și pot fi folosite de către //t1//. A se observa că, față de metoda //start()//, metoda //join()// blochează execuția thread-ului care o apelează (//t1//) până când //t2// își termină execuția. Spunem că //join()// este o __metodă blocantă__. | + | |
| - | + | ||
| - | === Lock === | + | |
| - | + | ||
| - | Lock-ul este un element de sincronizare care oferă acces exclusiv la __porțiunile de cod protejate de către lock__ (cu alte cuvinte definește o secțiune critică). Python pune la dispoziție clasa //Lock// pentru a lucra cu acest element de sincronizare. Un obiect de tip //Lock// se poate afla într-una din următoarele două stări: **blocat** sau **neblocat**, implicit, un obiect de tip //Lock// fiind creat în starea **neblocat**. Sunt oferite două operații care controlează starea unui lock: //acquire()// și //release()//. | + | |
| - | + | ||
| - | * //acquire()// va trece lock-ul în starea blocat. Dacă lock-ul se afla deja în starea blocat, thread-ul care a apelat //acquire()// se va bloca până când lock-ul este eliberat (pentru a putea fi blocat din nou). | + | |
| - | * //release()// este cea care trece lock-ul în starea deblocat. Cele două metode garantează că un singur thread poate deține lock-ul la un moment dat, oferind astfel posibilitatea ca un singur thread să execute secțiunea de cod critică. O altă garanție a lock-ului este că toate rezultatele thread-ului care a efectuat //release()// sunt disponibile și pot fi folosite de următoarele thread-uri care execută //acquire()//. | + | |
| - | + | ||
| - | Lock-ul este utilizat în majoritatea cazurilor pentru a **proteja accesul la structuri de date partajate**, care altfel ar putea fi modificate de un fir de execuție în timp ce alte fire de execuție încearcă simultan să citească sau să modifice și ele aceeași structură de date. Pentru a rezolva această situație, porțiunile de cod care accesează structura de date partajată sunt încadrate între apeluri //acquire()// și //release()// pe **același** obiect //Lock// partajat de toate thread-urile care vor sa acceseze structura. | + | |
| - | + | ||
| - | <note important> | + | |
| - | Spre deosebire de un mutex (ex: [[https://computing.llnl.gov/tutorials/pthreads/#Mutexes | pthread_mutex]]), în Python, metodele //acquire()// și //release()// pot fi apelate de thread-uri diferite. Cu alte cuvinte un thread poate face //acquire()// și alt thread poate face //release()//. Datorită acestei diferențe subtile nu este recomandat să folosiți un obiect //Lock// în acest mod. Pentru a reduce confuziile și a obține același efect se poate folosi un obiect //BoundedSemaphore// inițializat cu valoarea 1. | + | |
| </note> | </note> | ||
| - | Exemplul de mai jos prezintă folosirea unui lock pentru a proteja accesul la o listă partajată de mai multe thread-uri. | + | ===== Cipuri ce conecteaza prin Hypertransport core-uri AMD ===== |
| - | <code python task03.py> | + | *AMD8131 Hypertransport PCI-X Tunnel: Are rol de bus cu PCI-X |
| - | from threading import Lock, Thread | + | *AMD8151 Hypertransport AGP Tunnel: Controller grafic AGP3.0 . Este practic ce a mai ramas din NorthBridge dupa integrarea controller-ului |
| + | *AMD8111 Hypertransport I/O Hub: Are functionalitate standard de SouthBridge, incluzand controller PCI, BIOS,USB,hard disk, retea si audio. | ||
| - | def inc(lista, lock, index, n): | + | {{ :asc:lab4:htchips.jpg?480 | Figura 8. Cipuri ce asigura interconectarea prin Hypertransport a core-ului}} |
| - | """ Incrementeaza elementul index din lista de n ori """ | + | |
| - | for i in range(n): | + | |
| - | lock.acquire() | + | |
| - | lista[index] += 1 | + | |
| - | lock.release() | + | |
| - | def dec(lista, lock, index, n): | + | ===== Raspunsul Intel la Hypertransport - QuickPath Interconnect ===== |
| - | """ Decrementeaza elementul index din lista de n ori """ | + | |
| - | for i in range(n): | + | |
| - | lock.acquire() | + | |
| - | lista[index] -= 1 | + | |
| - | lock.release() | + | |
| - | # lista si lock-ul care o protejeaza | + | |
| - | my_list = [0] | + | |
| - | my_lock = Lock() | + | |
| - | # thread-urile care modifica elemente din lista | + | In mod evident Intel nu putea ramane indiferent avantajelor oferite de sistemul de interconectare oferit de catre HyperTransport. Si astfel putem vedea in figura urmatoare cum a aparut QuickPath Interconnect: |
| - | t1 = Thread(target = inc, args = (my_list, my_lock, 0, 100)) | + | |
| - | t2 = Thread(target = dec, args = (my_list, my_lock, 0, 100)) | + | |
| - | # lista inainte de modificari | + | {{ :asc:lab4:fsb_evolution.jpg?640 |Figura 9. Evolutia Front-side-Bus-ului in sistemele Intel}} |
| - | print (my_list) | + | |
| - | t1.start() | + | Acest sistem de interconectare este practic identic functional cu HyperTransport. Intre timp, procesoarele Intel au incorporat si ele controller-ul de memorie, si astfel cei doi mari competitori pe piata procesoarelor de uz general sunt pregatiti in aceeasi masura pentru sisteme multi-core cu multe procesoare, memorie multa si aplicatii multi-threading pe scara larga: |
| - | t2.start() | + | |
| - | t1.join() | + | {{ :asc:lab4:intel-qpi-1.jpg?640 |Figura 10. Arhitectura Intel QuickPath Interconnect }} |
| - | t2.join() | + | |
| - | # lista dupa modificari | + | Sistemul QuickPath ofera o rata de transfer de pana la 25.6GB/s pentru fiecare port (pereche de linii). Legatura poate fi utilizata atat pentru interconectarea controller-ului I/O cu CPU-urile, sau a CPU-urilor intre ele. Astfel atat sisteme mono- cat si multi-procesor pot fi realizate cu usurinta cu acest tip de interconectare. Mai multe detalii pot fi vazute in figura urmatoare: |
| - | print (my_list) | + | |
| - | </code> | + | |
| - | <note tip> | + | {{ :asc:lab4:intel-qpi-2.jpg?640 |Figura 11. Exemple de utilizare a QPI pentru sisteme mono si multi-procesor}} |
| - | Puteți folosi construcția //with// pentru a delimita o secțiune critică astfel: | + | |
| - | <code> | + | |
| - | def inc(lista, lock, index, n): | + | |
| - | for i in range(n): | + | |
| - | with lock: | + | |
| - | lista[index] += 1 | + | |
| - | </code> | + | |
| - | </note> | + | |
| - | === Obiecte thread-safe === | + | In mod similar cu sistemul Hypertransport, si interconectarea QuickPath poate fi utilizata pentru a lega sisteme HPC cu mai multe procesoare si siteme de intrare iesire, dupa cum se poate observa in urmatoarea figura: |
| - | Anumite operații pe obiecte din Python (tipuri primitive, liste, dictionare) sunt atomice, asta însemnând că nu trebuie protejate prin obiecte de sincronizare dacă acel obiect este partajat de mai multe threaduri. Lista lor o găsiți [[https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe|aici]]. | + | {{ :asc:lab4:qpi-cpu-mem-io.gif?480 |Figura 12. Conectarea Utilizand QPI a mai multor procesoare memorii si sisteme I/O}} |
| - | Listele de exemplu sunt protejate de către lock-ul global al interpretorului (GIL) și sunt considerate thread-safe. Atenție, asta nu înseamnă că orice operație care are legătură cu o listă este safe! În exemplul din secțiunea [[.:02#lock|Lock]] operația de modificare a elementului listei nu este atomică și a fost nevoie să protejăm prin lock-uri pentru a nu corupe lista. | + | ===== Sisteme Multiprocesor Intel si AMD ===== |
| - | <code Python> | + | Familia Hammer a fost creata pentru a putea oferi un multiprocesor scalabil, eficient din punctul de vedere al pretului raportat la numarul de procesoare. AMD a mai avut o tentativa in trecut de a crea procesoare pentru sisteme multiprocesor, cu AthlonMP. Desi acesta nu a fost o reusita de piata, datorita lui AMD am putut studia problemele aparute in astfel de sisteme. La Athlon MP, memoria (care era partajata) era botleneck-ul principal. Fiind memorie partajata, toate procesoarele imparteau FSB (Front Side Bus). Cu alte cuvinte viteza cu care procesoarele puteau teoretic accesa memoria era mult mai mare decat viteza cu care putea fi aceasta accesata. Solutia de la Hammer ar fi fost sa ofere fiecarui CPU propria sa conexiune la North Bridge, dar acest lucru ar fi fost foarte scump. Solutia relativ ieftina si care nu are nici penalizari de performanta a fost includerea controller-ului de memorie in procesor, ceea ce s-a si facut. Astfel, fiecare procesor are propria sa legatura de 128 biti cu memoria, avand pana la 5.3 Gbytes/sec. |
| - | >>> my_list = [1,2] | + | |
| - | >>> my_list.append(0) # safe | + | |
| - | >>> my_list[1] += 1 # not safe, += nu este operatie atomica! | + | |
| - | >>> my_list | + | |
| - | [1, 3, 0] | + | |
| - | >>> my_list.sort() # safe | + | |
| - | >>> my_list | + | |
| - | [0, 1, 3] | + | |
| - | </code> | + | |
| - | === RLock === | + | {{ :asc:lab4:amdvsmp.jpg?640 |Figura 13. Sistem AMD vs sisteme Muti Procesor Clasice}} |
| - | RLock-ul (Re-entrant Lock) este un element de sincronizare similar cu Lock, dar care oferă posibilitatea de a accesa o secțiune critică de mai multe ori din același thread. Accesările structurilor de date partajate sunt încadrate între apelurile //acquire()// și //release()//, la fel ca și în cazul Lock-ului. | + | In plus, datorita hypertransport, fiecare procesor poate accesa memoria celorlate procesoare la viteze de 3.2Gbytes/sec. Datorita acestui fapt, implementarea unui sistem dual-procesor e la fel de "usoara" ca a unuia cu 8 procesoare, deoarece partile componenete sunt scalabile prin utilizarea Hypertransport. AMD numeste acesta abordare "glueless multiprocessing", deoarece procesoarele sunt legate slab prin Hypertransport. De fapt, e diferenta dintre o cuplare puternica gen circuit-switched versus o cuplare slaba, gen packet-switched, cum se intampla in cazul de fata. Figura de mai jos face o comparatie intre arhitecturile de la Intel (Xeon) si AMD (Athlon/Opteron). |
| - | Intern sunt folosite concepte precum //owning thread// și //recursion level// pe lângă starea lock-ului de blocat/neblocat. În momentul în care un anumit thread blochează lock-ul, el, și numai el, îl poate re-bloca, fără să intre în deadlock și fără a-l elibera în prealabil. Pentru a elibera secțiunea/lock-ul, thread-ul va trebui să apeleze //release()// de un număr de ori egal cu numărul de apeluri //acquire()//. | + | {{ :asc:lab4:multiprocs.gif?640 |Figura 14. Comparatie intre sisteme multiprocesor Intel si AMD}} |
| - | RLock-ul devine util însă în momentul folosirii unor secțiuni critice imbricate, cauzate de funcții recursive, sau pur și simplu de organizarea codului în funcții multiple, care se apelează reciproc. În cazul folosiri RLock-ului nu mai este nevoie de urmărirea manuală stării lock-ului pentru a evita deadlock-ul ce apare la imbricarea secțiunilor critice definite de un Lock simplu. | + | Se observa ca cele doua procesoare Athlon (a) impart acelasi controller de memorie. Desi aceasta abordare nu are repercusiuni asupra performantei, sistemul nu e scalabil, adica pentru un sistem cu 3 procesoare ar trebui creat un controller separat. Este practic un sistem puternic cuplat (circuit-switched). Cum am mai mentionat, sistemele puternic cuplate sunt greu de scalat. In sistemul (b) cu Intel Xeon, procesoarele impart FSB-ul care, dupa cum am arata mai sus, duce la un botleneck semnificativ. In final, la (cde) avem sisteme cu Hammer Opteron. Acestea, fiind slab cuplate prin HyperTransort sunt usor de scalat la 2, 4 sau 8 procesoare. In plus, fiecare are propria sa legatura la memorie, neaparand botleneckuri, ca in cazul Intel Xeon. |
| - | <code python task04.py> | + | ===== Comparatie intre servere AMD si Intel ===== |
| - | from threading import RLock, Thread | + | |
| - | def inc(lista, rlock, index, n): | + | Generatia Phenom de microprocesoare de la AMD este competitorul direct al Intel Xeon si Intel Itanium. Cele doua arhitecturi, Intel versus AMD sunt fundamental diferite, dar ofera performante comparabile, in functie de domeniul de aplicatie ales pentru comparatie. |
| - | """ Incrementeaza elementul index din lista de n ori """ | + | |
| - | rlock.acquire() | + | |
| - | if n > 0: | + | |
| - | lista[index] += 1 # incrementeaza o data | + | |
| - | inc(lista, rlock, index, n - 1) # incrementeaza recursiv de n-1 ori | + | |
| - | rlock.release() | + | |
| - | def dec(lista, rlock, index, n): | + | O prima mare diferenta intre cele doua arhitecturi este modul in care cele doua abordeaza compatibilitatea cu 32 de biti. Astfel, de la Hammer incoace, AMD a ales sa extinda setul actual de instructiuni x86 pentru 32biti cu instructiuni pentru 64, in timp ce Intel a renuntat complet la setul x86, trecand in mod radical la IA64. Compatibilitatea la AMD este asigurata automat, noul set de instructiuni fiind doar o extensie e acelui vechi. La Intel, compatibilitatea cu 32 biti se face prin emularea vechiului set. Fiind o emulare, exista penalizari de performanta. Pe de alta parte insa, Intel a reusit sa scape in acest fel de complicatii de arhitectura inutile intr-o lume numai de 64 biti. Ambele tipuri mari de arhitecturi de procesoare, Intel si AMD, au trecut in ultimele generatii de procesoare de la emularea setului de instructiuni x86 catre micro-operatii specifice fiecarei generatii de procesoare. |
| - | """ Decrementeaza elementul index din lista de n ori """ | + | |
| - | rlock.acquire() | + | |
| - | if n > 0: | + | |
| - | lista[index] -= 1 # decrementeaza o data | + | |
| - | dec(lista, rlock, index, n - 1) # decrementeaza recursiv de n-1 ori | + | |
| - | rlock.release() | + | |
| - | # lista si lock-ul care o protejeaza | + | O alta diferenta este legatura dintre performanta, viteza ceasului si gradul de paralelism oferit. Astfel, la inceput Intel s-a concentrat pe marirea frecventei de ceas, si mai putin pe efectuarea de mai multe operatii in paralel. AMD ofera frecvente mai mici, dar a pus mult accent pe paralelism. Astfel performanta e oferita de AMD la frecvente mult mai mici decat Intel. Un avantaj al acestui fapt este ca AMD elimina astfel problemele de disipare a caldurii. Aceste aspecte sunt esentiale mai ales in domeniul serverelor. In ultimii ani Intel merge pe aceeasi cale, si se axeaza in principal pe cresterea numarului de core-uri si nu a frecventei de procesare, cu aceleasi avantaje mentionate anterior. |
| - | my_list = [0] | + | |
| - | my_lock = RLock() | + | |
| - | # thread-urile care modifica elemente din lista | + | {{ :asc:lab4:amdvsintel_2procs.jpg?800 |Figura 15. Comparatie intre servere multiprocesor Intel si AMD cu doua procesoare}} |
| - | t1 = Thread(target = inc, args = (my_list, my_lock, 0, 100)) | + | {{ :asc:lab4:amdvsintel_4procs.jpg?800 |Figura 16. Comparatie intre servere multiprocesor Intel si AMD cu patru procesoare}} |
| - | t2 = Thread(target = dec, args = (my_list, my_lock, 0, 100)) | + | |
| - | # lista inainte de modificari | + | O alta diferenta e numarul de registre generale; acest aspect are efecte imediate asupra costului de productie. Intel are 128 de registre pentru numere intregi si 128 pentru numere in virgula mobila, in timp ce AMD are numai 16 registre generale. AMD a decis aceasta abordare in urma constatarii ca 80% din cod foloseste maxim 16 registre. Aceasta abordare i-a permis sa reduca semnificativ costurile. |
| - | print (my_list) | + | |
| - | t1.start() | + | In sfarsit, o serie de exemple de utilizare ale sistemului de interconectare QuickPath pot fi observate in urmatoarea figura. Aici procesoare din generatia Xeon - modelele Bloomfield si Nehalem - sunt legate de controller-ele placilor de baza si de sistemele de intrare - iesire prin legaturi Intel QuickPath. Acelasi tip de interconectare este utilizat si pentru legatura mai multor chipseturi intre ele. |
| - | t2.start() | + | |
| - | t1.join() | + | {{ :asc:lab4:qpi-vs-procs.gif?800 |Figura 17. Exemple de utilizare a QPI impreuna cu arhitecturi de procesoare Intel (Bloomfield / Nehalem)}} |
| - | t2.join() | + | |
| - | # lista dupa modificari | + | Dupa cum se poate observa, scalabilitatea sistemelor de calcul de-a lungul vremii este dependenta puternic de solutiile de interconectare a elementelor de procesare. Astfel, de la magistrale simple, la magistrale multiple, la solutii complexe si scalabile cum sunt Hypertransport si QuickPath Interconnect, toate s-au dezvoltat in paralel cu modelele de procesoare si chipseturi pe care le interconecteaza. |
| - | print (my_list) | + | |
| - | </code> | + | |
| - | Un exemplu despre diferența de funcționare între Lock și RLock la nivel de apelare a funcțiilor | + | ===== Exerciții ===== |
| - | ^ //Lock// ^ //RLock// ^ | + | |
| - | |<code> | + | |
| - | import threading | + | |
| - | lock = threading.Lock() | + | Laboratorul poate fi realizat atat pe sistemul propriu (linux) cat si pe clusterul nostru, accesibil prin: fep.grid.pub.ro astfel: |
| + | - ''ssh -Y username@fep.grid.pub.ro'' (puneti utilizatorul vostru in loc de username) | ||
| + | - ''%%srun --x11 -p nehalem --pty /bin/bash%%'' - va permite conectarea pe coada Nehalem cu 14 servere. Alternativ puteti folosi scriptul de la laboratorele de GPU-uri pentru a crea imaginea de mai jos in mod neinteractiv pe nodurile din coada nehalem. | ||
| + | - ''%%apptainer run docker://gitlab.cs.pub.ro:5050/asc/asc-public/c-labs:1.3.1 /bin/bash%%'' - accesăm imaginea de docker în cadrul căreia avem permisiunile necesare realizării laboratorului | ||
| + | - ''%%wget https://ocw.cs.pub.ro/courses/_media/asc/lab4/lab4_skl.tar.gz%%'' va permite downloadul arhivei de laborator pe cluster. | ||
| + | - ''%%tar xvfz lab4_skl.tar.gz%%'' realizeaza dezarhivarea containerului tar.gz unde se afla laboratorul. Puteti acum explora intregul schelet al laboratorului si rezolva taskurile specificate in TASKS.md. | ||
| - | print ('First try :', lock.acquire()) | + | Alte comenzi utile: |
| - | print ('Second try:', lock.acquire()) | + | - ''sinfo'' - va arata cozile existente |
| + | - ''squeue'' - va arăta informațiile despre joburile în execuție | ||
| + | - ''sbatch -p nehalem ./script.sh'' - va lansa în execuție un task pe una dintre cozi | ||
| + | - ''srun -p nehalem --pty -w nehalem-wn17 /bin/bash'' permite conectarea pe un anumit nod din cadrul unei cozi (17 in acest caz), si nu doar conectarea pe orice nod dintr-o coada | ||
| - | print ("print this if not blocked...") | + | <note important> |
| - | </code>|<code> | + | Dacă folosiți WSL2 urmați indicațiile de [[:asc:res:wsl-x11]] |
| - | import threading | + | </note> |
| - | lock = threading.RLock() | ||
| - | print ('First try :', lock.acquire()) | + | **Task 0** - Alocați un vector de elemente ''struct particle'' în următoarele moduri: global, pe stivă și dinamic. |
| - | print ('Second try:', lock.acquire()) | + | * Porniți de la ''task01.c'' (pentru alocare globală), ''task02.c'' (pentru alocare pe stivă) și, respectiv, ''task03.c'' (pentru alocare dinamică). |
| + | * Pentru compilare utilizați fișierul Makefile și comanda ''make task0<n>''. | ||
| + | * Compilați și rulați executabilele generate pentru un număr din ce în ce mai mare de elemente alocate în vector - porniți de la 1,000,000. | ||
| + | * Câte elemente pot fi alocate maxim prin fiecare metodă? Utilizati comanda ''size <executabil>'' pentru a vedea ''bss'' unde se afla stocate variabilele globale. | ||
| + | * Pentru a creste dimensiunea stack-ului utilizat de sistem puteti folosi ''ulimit -s unlimited''. Pentru vizualizare si verificare a limitelor sistemului puteti incerca ''ulimit -a''. | ||
| + | * Pentru a face verificarile codului mai rapide, parcurgeti vectorul cu verificarea vitezei din 5M in 5M - nu pentru fiecare valoare in parte. Exercitiul este despre alocare. | ||
| - | print ("print this if not blocked...") | + | **Task 1** - Alocați dinamic o matrice de elemente ''struct particle'', in mod liniarizat. Populați aleator matricea astfel încât liniile pare să conțină particule care au componentele vitezei pozitive, iar liniile impare să conțină particule care au componentele vitezei negative. Scalați apoi vitezele tuturor particulelor cu 0.5, ignorând structura matricii, prin folosirea unui cast. Introduceti o verificare pentru alocarea dinamica vs. alocarea "clasica" pe linii. Porniti de la ''task1.c'' |
| - | </code> | | + | |
| - | | **Output** || | + | |
| - | | <code> | + | |
| - | First try : True | + | |
| - | Second try: | + | |
| - | </code> | <code> | + | |
| - | First try : True | + | |
| - | Second try: print this if not blocked... | + | |
| - | </code>| | + | |
| - | === Semaphore === | + | **Task 2** - Studiați alinierea variabilelor în C in fisierul ''task2.c'' |
| + | * Afișati adresele variabilelor declarate în schelet. Ce observați despre aceste adrese? Ce legatură există între acestea și dimensiunea tipului? | ||
| + | * Calculați dimensiunea structurilor a si b din scheletul de program. Afișati dimensiunea acestora folosind ''sizeof''. Explicați cele observate. | ||
| + | * Studiați exemplul de aliniere manuală la un multiplu de 16 sau 32 bytes. In cazul in care lucrati pe o arhitectura de 32 de biti, explicati rezultatele observate. | ||
| + | * Studiati efectul optimizarilor de compilator -O2 sau -O3 asupra alinierii datelor. | ||
| - | Semaforul este un element de sincronizare cu o interfață asemănătoare Lock-ului (metodele //acquire()// și //release()//) însă cu o comportare diferită. Python oferă suport pentru semafoare prin intermediul clasei //Semaphore//. | + | **Task 3** - Determinați dimensiunea cache-urilor L1 și L2 folosindu-vă de metoda prezentată în curs ({{asc:lab4:asc_-_03_-_ierarhia_de_memorie_si_optimizarea_pentru_memorie.pdf | slide-urile 67-77}}). Utilizati variabile char/int8_t pentru acest task. |
| + | * Folosiți makefile-ul pus la dispoziție pentru a genera graficele. | ||
| + | * Porniti de la ''task3.c'' | ||
| - | Un //Semaphore// menține un contor intern care este decrementat de un apel //acquire()// și incrementat de un apel //release()//. Metoda //acquire()// nu va permite decrementarea contorului sub valoarea 0, ea blocând execuția thread-ului în acest caz până când contorul este incrementat de un //release()//. Metodele //acquire()// și //release()// pot fi apelate fără probleme de thread-uri diferite, această utilizare fiind des întâlnită în cazul semafoarelor. | ||
| - | Un exemplu clasic de folosire a semaforului este acela de a limita numărul de thread-uri care accesează concurent o resursă precum în exemplul următor: | + | **Task 4** - Determinați dimensiunea unei linii de cache folosindu-vă de metoda prezentată în curs ({{asc:lab4:asc_-_03_-_ierarhia_de_memorie_si_optimizarea_pentru_memorie.pdf | slide-urile 67-77}}). |
| + | * Folosiți makefile-ul pus la dispoziție pentru a genera graficele. Utilizati variabile char/int8_t pentru acest task. | ||
| + | * Porniti de la ''task4.c'' | ||
| + | * Generați grafice pentru mai multe dimensiuni ale vectorului parcurs astfel încât să depășească mărimea cache-ului L1, L2, respectiv, L3. | ||
| + | * Mecanismele hardware avansate implementate în arhitecturile de procesoare actuale generează comportamente complexe care nu corespund nepărat modelului simplu prezentat în curs. Aceste mecanisme pot chiar masca dimensiunea reală a linei de cache, fiind astfel necesară testarea cu diferite valori ale vectorului parcurs pentru a putea trage o concluzie informată. | ||
| + | * Studiati si explicati de ce codurile (identice) si makefile-urile (diferite) de la taskurile 3 si 4 duc la graficele obtinute in cadrul laboratorului. | ||
| + | * Combinati intr-un singur grafic "relevant" rezultatele prezentate in taskurile 3 si 4. | ||
| - | <code python task05.py> | ||
| - | from random import randint, seed | ||
| - | from threading import Semaphore, Thread | ||
| - | from time import sleep | ||
| - | def access(nr, sem): | + | /* |
| - | sem.acquire() | + | - Studiați și documentați cum poate fi utilizată dimensiunea determinată mai sus în contextul rezolvării unor probleme simple de genul: înmulțirea unui vector cu un scalar sau înmulțirea a doi vectori (fragment de cod relevant pe slide-ul 77). Dimensiunea acestor vectori va varia între limitele următoare: |
| - | print ("Thread-ul", nr, " acceseaza") | + | * Mai mici decât cache-ul sistemului (trebuie determinat de dumneavoastră din specificațiile sistemului). |
| - | sleep(randint(1, 4)) | + | * Mai mari decât cache-ul și mai mici decât memoria principala a sistemului de calcul din laborator (aceasta din urmă o determinați dumneavoastră din specificațiile sistemului). |
| - | print ("Thread-ul", nr, " a terminat") | + | * Mai mare decât memoria principala a sistemului. |
| - | sem.release() | + | */ |
| - | # initializam semaforul cu 3 pentru a avea maxim 3 thread-uri active la un moment dat | + | ==== Tips ==== |
| - | semafor = Semaphore(value = 3) | + | Pentru verificarea rezultatelor obtinute pentru dimensiunea cache-ului puteti folosi si rezultatele obtinute cu: |
| + | * <code bash> | ||
| + | [student@localhost ~]$ getconf -a | ||
| + | [student@localhost ~]$ cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size | ||
| + | [student@localhost ~]$ cat /proc/cpuinfo | ||
| + | </code> | ||
| + | * <code bash> | ||
| + | [student@localhost ~]$ sudo dmidecode | less | ||
| + | </code> | ||
| - | # stocam obiectele Thread pentru a putea face join | + | Cautati dupa "Cache" si veti afla informatiile despre memoria cache instalata |
| - | thread_list = [] | + | |
| - | seed() # seed-ul este current system time pentru generatorul de nr random | + | * <code bash> |
| - | + | [student@localhost ~]$ valgrind --tool=cachegrind ./program_test | |
| - | # pornim thread-urile | + | |
| - | for i in range(10): | + | |
| - | thread = Thread(target = access, args = (i, semafor)) | + | |
| - | thread.start() | + | |
| - | thread_list.append(thread) | + | |
| - | + | ||
| - | # asteptam terminarea thread-urilor | + | |
| - | for i in range(len(thread_list)): | + | |
| - | thread_list[i].join() | + | |
| </code> | </code> | ||
| - | ===== Exerciții ===== | + | Acesta va simula rularea programului si va afisa informatii legate de accesul la L1, L2 cache si miss rate. |
| + | Un tutorial interesant si util pentru utilizarea gdb poate fi gasit in pagina echipei de SO, aici: http://ocw.cs.pub.ro/courses/so/laboratoare/resurse/gdb | ||
| - | **Task 1** - Rulați exemplele task01.py task02.py task03.py task04.py task05.py. | + | ===== Resurse ===== |
| + | * Responsabilii acestui laborator: [[emil.slusanschi@cs.pub.ro|Emil Slușanschi, Cosmin Samoila]] | ||
| + | * <html><a class="media mediafile mf_pdf" href=":asc:lab4:index?do=export_pdf">PDF laborator</a></html> | ||
| + | * {{asc:lab4:lab4_skl.tar.gz|Schelet laborator}} | ||
| + | <hidden> * {{:asc:lab4:sol:lab4_sol.tar.gz|Soluție Laborator 4}} </hidden> | ||
| - | **Task 2** - Creați și rulați threaduri urmărind cerințele din fișierul ''task2.py'' din scheletul de laborator. | ||
| - | **Task 3** - Problema producător-consumator folosind semafoare. | ||
| - | * În task3.py este dată o implementare sumară a clasei Coffee și a unei clase ExampleCoffee. Folosind ca model, clasa ExampleCoffee, realizați alte 3 implementări pentru următoarele tipuri de cafea: Espresso, Americano și Cappuccino, care vor trebui să moștenească clasa de bază Coffee. | ||
| - | * CoffeeFactory funcționează ca un producător, iar User ca un consumator și vor trebui să realizeze operațiunile de produce și consume, mereu, fără blocaje. | ||
| - | * Implementați toate aceste clase, precum și clasa Distributor, care deține un buffer limitat la un număr fix de cafele, precum și de elementele de sincronizare necesare. | ||
| - | * **Important:** Implementarea trebuie să funcționeze pentru __**oricâți**__ producători și __**oricâți**__ consumatori, numere care vor fi propuse de către fiecare asistent, în momentul verificării. | ||
| - | * **Hint:** De câte semafoare am avea nevoie? | ||
| - | |||
| - | |||
| - | **Task 4** - Implementați problema filozofilor. | ||
| - | * Se consideră mai mulți filozofi ce stau în jurul unei mese rotunde. În mijlocul mesei este o farfurie cu spaghete. Pentru a putea mânca, un filozof are nevoie de două bețisoare. Pe masă există câte un bețișor între fiecare doi filozofi vecini. Regula este că fiecare filozof poate folosi doar bețișoarele din imediata sa apropriere. Trebuie evitată situația în care nici un filozof nu poate acapara ambele bețișoare. Comportamentul tuturor filozofilor trebuie să fie identic. | ||
| - | |||
| - | |||
| - | |||
| - | <note important> | ||
| - | Puteți găsi materiale ajutătoare în cadrul [[https://ocw.cs.pub.ro/courses/apd/laboratoare/05|laboratorului 5 de la APD]]</note> | ||
| - | |||
| - | <note important> | ||
| - | Codul protejat (încadrat) de lockuri nu e bine să conțină //print-uri// sau instrucțiuni ce nu lucrează cu variabila partajată (e.g. //sleep//). | ||
| - | |||
| - | Nu este recomandat ca într-o aplicație concurentă să aveți print-uri, și nici lock-uri pe print-uri, acest lucru afectând comportarea threadurilor. În laborator se folosesc print-uri în scop de debugging, și vă recomandăm să folosiți //format// sau concatenare (//+//) pentru a obține afișări atomice. | ||
| - | </note> | ||
| - | |||
| - | ===== Resurse ===== | ||
| - | * <html><a class="media mediafile mf_pdf" href=":asc:lab2:index?do=export_pdf">PDF laborator</a></html> | ||
| - | * {{:asc:laboratoare:lab02-skl.zip|Schelet laborator}} | ||
| - | <hidden> | ||
| - | * {{.:lab2-sol.zip|Soluție laborator}} | ||
| - | </hidden> | ||
| ==== Referințe ==== | ==== Referințe ==== | ||
| - | * [[https://realpython.com/oop-in-python-vs-java/ | OOP in Python vs Java]] | + | * [[http://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86 | Alinierea structurilor in C pe x86]] |
| - | + | * {{:asc:lab4:amd_cpu_roadmap.pdf|}} | |
| - | **Documentație module** | + | * {{:asc:lab4:amd-quad-core_opteron_2proc_server-ws_comparison.pdf|}} |
| - | + | * {{:asc:lab4:amd-4proc_server_comparison.pdf|}} | |
| - | * [[http://docs.python.org/3/library/threading.html| modulul threading]] - Thread, Lock, Semaphore | + | * {{:asc:lab4:amd_designing_for_n_cores.pdf|}} |
| - | * [[https://docs.python.org/3/library/_thread.html#module-_thread| modulul _thread]] | + | * {{:asc:lab4:introduction_to_amd64_2005.pdf|}} |
| - | + | * {{:asc:lab4:coleamd.pdf|}} | |
| - | **Detalii legate de implementare** | + | * {{:asc:lab4:multi-core_codingphenom.pdf|}} |
| - | + | * {{:asc:lab4:opteronoverview.pdf|}} | |
| - | * [[https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe| What kinds of global value mutation are thread-safe]] | + | * {{:asc:lab4:318082.pdf|}} |
| - | * [[http://dabeaz.blogspot.ro/2009/09/python-thread-synchronization.html|Implementarea obiectelor de sincronizare în CPython]] | + | * {{:asc:lab4:opteron_mpf.pdf|}} |
| - | * [[https://realpython.com/python-gil/| What is the Python Global Interpreter Lock (GIL)?]] | + | * {{:asc:lab4:opteron_data_sheet.pdf|}} |
| - | * [[https://opensource.com/article/17/4/grok-gil | Grok the GIL: How to write fast and thread-safe Python]] | + | * {{:asc:lab4:xeon-5600-brief.pdf|}} |
| - | * [[http://www.dabeaz.com/python/UnderstandingGIL.pdf|Understanding the Python GIL]] (prezentare foarte bună și amuzantă) | + | * {{:asc:lab4:xeon-5600-vol-1-datasheet.pdf|}} |
| - | + | * {{:asc:lab4:xeon-5600-vol-2-datasheet.pdf|}} | |
| - | **Despre concurență și obiecte de sincronizare** | + | * {{:asc:lab4:nv_ds_tesla_c2050_c2070.pdf|}} |
| - | + | * {{:asc:lab4:tesla-kseries-overview-lr.pdf|}} | |
| - | * [[http://preshing.com/20150316/semaphores-are-surprisingly-versatile/|Versatilitatea Semafoarelor]] | + | * [[http://blog.regehr.org/archives/898| Sequential Consistency vs TSO]] |
| - | * [[http://linuxgazette.net/107/pai.html |Understanding Threading in Python]] | + | * [[http://www.catb.org/esr/structure-packing/ | The Lost Art of C Structure Packing]] |
| - | * <html><a class="media mediafile mf_pdf" href="https://www.google.ro/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&cad=rja&uact=8&ved=0ahUKEwi-k9vC5sbSAhUGiCwKHQK4CKYQFgghMAE&url=http%3A%2F%2Fgreenteapress.com%2Fsemaphores%2FLittleBookOfSemaphores.pdf&usg=AFQjCNGZLhXbpHIWv9OlMLaBdb3u2XVXyQ&sig2=FEcdccnj-DF3X26Dp2dqQQ">Little book of semaphores</a></html> | + | |
| - | /** {{.:parprocbook.pdf|Programming on Parallel Machines }}(Chapter 3) */ | + | |