2009-04-03 17 views
27

Durante l'apprendimento del "linguaggio assembler" (in linux su un'architettura x86 che utilizza GNU come assemblatore), uno dei momenti aha era la possibilità di utilizzare system calls. Queste chiamate di sistema sono molto utili e talvolta sono addirittura necessarie come il tuo programma runs in user-space.
Tuttavia le chiamate di sistema sono piuttosto costose in termini di prestazioni in quanto richiedono un interrupt (e, naturalmente, una chiamata di sistema), il che significa che deve essere eseguito un cambio di contesto dal programma attivo corrente nello spazio utente al sistema in esecuzione nel kernel- spazio.È possibile creare thread senza chiamate di sistema nell'assembly GAS Linux x86?

Il punto che voglio fare è questo: attualmente sto implementando un compilatore (per un progetto universitario) e una delle funzionalità extra che volevo aggiungere è il supporto per il codice multi-threaded per migliorare le prestazioni del programma compilato. Poiché parte del codice multi-threaded verrà automaticamente generato dal compilatore stesso, questo garantirà quasi che ci siano anche minuscoli bit di codice multi-thread in esso. Per ottenere una vittoria in termini di prestazioni, devo essere sicuro che l'utilizzo di thread renderà questo possibile.

Il mio timore tuttavia è che, per utilizzare la filettatura, I deve effettuare chiamate di sistema e gli interrupt necessari. I minuscoli thread (generati automaticamente) saranno quindi molto influenzati dal tempo necessario per effettuare queste chiamate di sistema, il che potrebbe addirittura portare a una perdita di prestazioni ...

La mia domanda è quindi duplice (con un extra domanda bonus sotto di esso):

  • E 'possibile scrivere assembler codice che può essere eseguito più thread contemporaneamente su più core in una volta, senza la necessità del sistema di chiamate?
  • Otterrò un guadagno in termini di prestazioni se ho thread davvero minuscoli (piccoli come nel tempo di esecuzione totale del thread), perdita di prestazioni o non vale la pena?

La mia ipotesi è che il codice assembler multithreaded è non possibile senza chiamate di sistema. Anche se questo è il caso, hai un suggerimento (o anche meglio: qualche codice reale) per implementare i thread nel modo più efficiente possibile?

+2

C'è un simile (anche se non duplicare IMHO) domanda qui: http://stackoverflow.com/questions/980999/what-does-multicore-assembly-language-look-like Le risposte ci potrebbe dare qualche insight –

risposta

19

La risposta breve è che non è possibile. Quando si scrive codice assembly, viene eseguito in sequenza (o con rami) su uno e solo un thread logico (cioè hardware). Se vuoi che parte del codice venga eseguito su un altro thread logico (sia sullo stesso core, su un core differente sulla stessa CPU o anche su una CPU diversa), devi avere il SO che imposta il puntatore di istruzioni dell'altro thread (CS:EIP) per puntare al codice che si desidera eseguire. Ciò implica l'uso di chiamate di sistema per far sì che il sistema operativo faccia ciò che desideri.

I thread utente non forniscono il supporto per il thread che si desidera, poiché vengono eseguiti tutti sullo stesso thread hardware.

Edit: La risposta di Incorporando Ira Baxter con Parlanse. Se si assicura che il proprio programma abbia un thread in esecuzione in ogni thread logico per cominciare, è possibile creare il proprio programmatore senza fare affidamento sul sistema operativo. Ad ogni modo, hai bisogno di un programmatore per gestire il salto da un thread all'altro. Tra le chiamate allo scheduler, non ci sono istruzioni di assemblaggio speciali per gestire il multithreading. Lo scheduler stesso non può fare affidamento su alcun assembly speciale, ma piuttosto su convenzioni tra parti dello scheduler in ogni thread.

In entrambi i casi, indipendentemente dal fatto che si utilizzi o meno il sistema operativo, è comunque necessario affidarsi a un programma di pianificazione per gestire l'esecuzione cross-thread.

+0

Ho contrassegnato la tua risposta come la risposta corretta; Stavo davvero cercando un modo per eseguire il codice simultaneamente su più core. Ho già accettato il fatto che ciò non fosse possibile nel modo in cui volevo che fosse ... Per caso, conosci il modo corretto per farlo? le informazioni su questo argomento sono piuttosto scarse. e molte grazie per la tua risposta! – sven

+0

In realtà è molto dipendente dal sistema operativo. Posso dirti come è fatto a livello di programmazione di sistema in x86, ma non so come farlo come utente in qualsiasi sistema operativo. –

+0

Probabilmente è possibile solo se si rilascia il sistema operativo, altrimenti è necessario passare attraverso i meccanismi forniti dal sistema operativo. – ShinTakezou

6

Implementare il threading in modalità utente.

Storicamente, i modelli di threading sono generalizzati come N: M, vale a dire N thread in modalità utente eseguiti su thread del modello M kernel. L'uso moderno è 1: 1, ma non è sempre stato così e non deve essere così.

È possibile conservare in un singolo thread del kernel un numero arbitrario di thread in modalità utente. È solo che è tua responsabilità passare da uno all'altro abbastanza spesso da sembrare tutto concomitante. I tuoi argomenti sono ovviamente cooperativi piuttosto che preventivi; in pratica hai diffuso le chiamate yield() per tutto il tuo codice per assicurarti che avvenga una commutazione regolare.

+1

Sì ...questo è l'unico modo gestibile per farlo e avere un miglioramento effettivo. I thread di sistema sono progettati per attività di lunga durata, non brevi bit di codice che sono multi-threaded solo per essere in grado di assorbire più tempo CPU. Attenzione al costo di mantenere la consistenza di mem, però ... – Varkhan

+1

L'idea che suggerisci sembra carina, ma come posso implementarla in assembler? quale sistema chiama/istruzioni assembler posso usare per questo? – sven

+2

La chiave è giocare con lo stack di chiamate. –

2

Per prima cosa si dovrebbe imparare come usare thred in C. Su GNU/Linux si vorrà probabilmente usare i thread Posix o i thread GLib. Quindi è possibile chiamare semplicemente C dal codice assembly.

Ecco alcune indicazioni:

  • thread POSIX: link text
  • Un tutorial dove potrete imparare come chiamare funzioni C dal gruppo: link text
+0

thread glib (linuxthread prima, NPTL quindi) sono thread POSIX, POSIX è solo una norma. – claf

2

chiamate di sistema non sono così lento ora con syscall o sysenter anziché int. Tuttavia, ci sarà solo un sovraccarico quando si creano o si distruggono i thread. Una volta che sono in esecuzione, non ci sono chiamate di sistema. I thread in modalità utente non ti aiuteranno, poiché funzionano solo su un core.

4

Se si desidera ottenere prestazioni, è necessario sfruttare i thread del kernel. Solo il kernel può aiutarti a far funzionare il codice contemporaneamente su più core della CPU. A meno che il tuo programma non sia legato all'I/O (o eseguendo altre operazioni di blocco), l'esecuzione del multithreading cooperativo in modalità utente (noto anche come fibers) non ti garantirà alcuna prestazione. Eseguirete solo interruttori di contesto extra, ma la CPU che il vostro thread reale è in esecuzione continuerà a funzionare al 100% in entrambi i modi.

Le chiamate di sistema sono diventate più veloci. Le moderne CPU supportano l'istruzione sysenter, che è significativamente più veloce della vecchia istruzione int. Vedi anche this article per come Linux fa chiamate di sistema nel modo più veloce possibile.

Assicurarsi che il multithreading generato automaticamente abbia i thread eseguiti per un tempo sufficiente per ottenere prestazioni. Non provare a parallelizzare brevi parti di codice, perderai tempo a generare spawn e unirmi ai thread. Fai attenzione anche agli effetti della memoria (anche se questi sono più difficili da misurare e prevedere) - se più thread accedono a set di dati indipendenti, verranno eseguiti molto più rapidamente rispetto a quando accedono ripetutamente agli stessi dati a causa del problema cache coherency.

+0

grazie per il tuo prezioso contributo! Sicuramente darò un'occhiata a "sysenter", ma rimane una domanda per me: come posso chiamare un kernel in assembler? e come posso essere sicuro che funzionerà su un nucleo separato? – sven

+0

Mentre l'ultima metà di questa risposta sembra sul punto, il bit su "usa i thread kernal" dove kernal significa "all'interno del sistema operativo" è semplicemente sbagliato. È necessario utilizzare semplici thread 'ol' (o processi aggiuntivi, se si può sopportare il tempo di commutazione di conext) per il quale Windows e Linux forniscono entrambe le chiamate. D'accordo, l'overhead di quelle chiamate è più alto di quello che vorrebbe. –

11

"Dottore, dottore, fa male quando lo faccio". Dottore: "Non farlo".

La risposta breve è che è possibile eseguire la programmazione multithreading senza chiamando costose primitive di gestione delle attività del sistema operativo. Ignora semplicemente il sistema operativo per le operazioni di pianificazione della filettatura . Ciò significa che è necessario scrivere il proprio programma di pianificazione e non passare mai il controllo al sistema operativo. (E devi essere in qualche modo più intelligente con il tuo thread in testa rispetto ai ragazzi piuttosto intelligenti del sistema operativo). Abbiamo scelto questo approccio proprio perché le chiamate in fibra di processo/thread/ di Windows erano troppo costose per supportare il calcolo dei grani di calcolo di alcune centinaia di istruzioni .

nostro langauge programmazione PARLANSE è un linguaggio di programmazione parallela: Vedi http://www.semdesigns.com/Products/Parlanse/index.html

PARLANSE gira sotto Windows, offre paralleli "grani" come l'astratto parallelismo costrutto, e gli orari tali grani da una combinazione di un grande sintonizzato scheduler scritto a mano e codice di programmazione generato dal compilatore PARLANSE che tiene conto del contesto della grana per minimizzare il sovraccarico di programmazione. Ad esempio, il compilatore assicura che i registri di una grana non contengano informazioni nel punto dove potrebbe essere necessaria la pianificazione (ad es. "Attesa") e quindi il codice dello schedulatore deve solo salvare PC e SP. In effetti, molto spesso il codice dello schedulatore non ottiene alcun controllo; un grano biforcuto memorizza semplicemente gli switch PC e SP forking, in stack preallocato dal compilatore e salta al codice di grano . Il completamento del grano riavvierà il forker.

Normalmente c'è un interblocco per sincronizzare i grani, implementato dal compilatore utilizzando le istruzioni native LOCK DEC che implementano ciò che equivale a contare i semafori. Le applicazioni possono forgiare logicamente milioni di grani; lo schedulatore limita i grani genitore generando più lavoro se le code di lavoro sono lunghe abbastanza, quindi un maggior lavoro non sarà utile. Lo scheduler implementa il furto del lavoro per consentire alle CPU affette da lavoro di afferrare i grani pronti formando le code di lavoro della CPU vicine. Questo ha stato implementato per gestire fino a 32 CPU; ma siamo un po 'preoccupati che i venditori di x86 potrebbero in effetti utilizzare l'acqua con più di che nei prossimi anni!

PARLANSE è un langauge maturo; lo abbiamo utilizzato dal 1997, e abbiamo implementato un'applicazione parallela di linea da diversi milioni.

+0

ciao, hai parlato in molti dei tuoi post, è effettivamente disponibile per gli utenti finali? Ho controllato gli esempi sulla tua pagina web (http://www.semdesigns.com/Products/Parlanse/examples.html) e sembra piuttosto LISPish? – none

+0

PARLANSE è disponibile, ma solo come parte del DMS Software Reengineering Toolkit. Sembra LISP ma non è LISP; no CAR o CDR ovunque! Il linguaggio di base è C-ish: scalari, strutture, puntatori, funzioni, ma lì diverge: nessun puntatore aritmetico, lambda con reali ambiti lessicali, stringhe dinamiche (UNICODE) e matrici, parallelismo (il punto principale di PARLANSE) e gestione delle eccezioni che funziona attraverso i confini del parallelismo. È possibile ottenere un migliore senso della lingua dal documento tecnico a http://www.semdesigns.com/Company/Publications/parallelism-in-symbolic-computation.pdf –

+1

@IraBaxter, come è anche possibile garantire "* non passare mai il controllo al sistema operativo *"? Il sistema operativo forzerebbe comunque un'interruzione, vero? – Pacerier

3

Un po 'tardi, ma ero interessato a questo tipo di argomento. In effetti, non c'è niente di speciale sui thread che richiedono specificamente il kernel per intervenire TRANNE per la parallelizzazione/le prestazioni.

obbligatorio Bluf:

Q1: No. Almeno chiamate di sistema iniziali sono necessari per creare più thread kernel tra i vari core CPU/iper-thread.

Q2: dipende. Se si creano/distruggono i thread che eseguono operazioni minuscole, si sprecano risorse (il processo di creazione dei thread supererebbe di molto il tempo utilizzato dal battistrada prima che esca).Se crei N thread (dove N è ~ # di core/hyper-thread sul sistema) e li ri-esegui, la risposta potrebbe essere sì a seconda dell'implementazione.

Q3: POTREBBE ottimizzare l'operazione se SAPIAVI in anticipo un metodo preciso di operazioni di ordinazione. Nello specifico, è possibile creare ciò che equivale a una catena ROP (o una catena di chiamate in avanti, ma in realtà potrebbe risultare più complessa da implementare). Questa catena ROP (come eseguita da un thread) eseguiva continuamente le istruzioni 'ret' (nel proprio stack) dove quella pila viene prepensionata in modo continuo (o aggiunta nel caso in cui viene spostata all'inizio). In tale modello (strano!) Lo schedulatore mantiene un puntatore alla "fine della catena ROP" di ogni thread e scrive nuovi valori su di esso in base al quale il codice circola attraverso il codice della funzione di esecuzione della memoria che alla fine risulta in un'istruzione ret. Ancora una volta, questo è un modello strano, ma è comunque intrigante.

Nel mio contenuto da 2 centesimi.

Ho recentemente creato ciò che effettivamente funziona come thread in puro assemblaggio gestendo varie regioni dello stack (create tramite mmap) e mantenendo un'area dedicata per memorizzare le informazioni di controllo/individualizzazione per i "thread". È possibile, anche se non l'ho progettato in questo modo, creare un unico grande blocco di memoria tramite mmap che suddivido in ciascuna area 'privata' di ciascun thread. Quindi sarebbe richiesto solo un singolo syscall (anche se le pagine di protezione tra sarebbero intelligenti queste richiederebbero syscalls aggiuntive).

Questa implementazione utilizza solo il thread del kernel di base creato quando il processo spawn e c'è solo un singolo thread di usermode durante l'intera esecuzione del programma. Il programma aggiorna il proprio stato e si programma da solo tramite una struttura di controllo interna. I/O e tali sono gestiti tramite opzioni di blocco quando possibile (per ridurre la complessità), ma questo non è strettamente richiesto. Naturalmente ho fatto uso di mutex e semafori.

per attuare tale sistema (completamente nello spazio utente e anche tramite un accesso non-root se desiderato) sono stati richiesti i seguenti:

Una nozione di ciò fili riducono a: Una pila per le operazioni di stack (pò autoesplicativa e ovvio) un insieme di istruzioni da eseguire (anche ovvio) un piccolo blocco di memoria per contenere i singoli contenuto del registro

che uno scheduler si riduce a: un manager per una serie di fili (si noti che i processi in realtà mai eseguire, solo il loro thread (s) fare) in un elenco ordinato specificato scheduler (di solito priorità).

Un commutatore di contesto thread: Un MACRO iniettato in varie parti del codice (di solito li metto al termine di funzioni pesanti) che equivale approssimativamente a "resa del thread", che salva lo stato del thread e carica un altro thread stato.

Quindi, è effettivamente possibile (interamente in assembly e senza chiamate di sistema diverse da mmap e mprotect iniziali) creare costrutti di tipo usermode thread in un processo non root.

Ho solo aggiunto questa risposta perché si menziona espressamente l'assembly x86 e questa risposta è stata interamente derivata da un programma autonomo scritto interamente in assembly x86 che raggiunge gli obiettivi (meno le funzionalità multi-core) di ridurre al minimo le chiamate di sistema e riduce al minimo overhead del thread lato sistema.