Esistono limiti tecnici o funzionalità del linguaggio che impediscono al mio script Python di essere veloce come un programma C ++ equivalente?


10

Sono un utente Python di vecchia data. Alcuni anni fa, ho iniziato a studiare il C ++ per vedere cosa poteva offrire in termini di velocità. Durante questo periodo, continuerei a usare Python come strumento per la prototipazione. Sembrava che questo fosse un buon sistema: sviluppo agile con Python, esecuzione veloce in C ++.

Recentemente, ho usato Python sempre di più, e ho imparato a evitare tutte le insidie ​​e gli anti-schemi che ero veloce da usare nei miei primi anni con il linguaggio. Comprendo che l'utilizzo di determinate funzionalità (comprensione di elenchi, enumerazioni, ecc.) Può migliorare le prestazioni.

Ma ci sono limiti tecnici o funzionalità del linguaggio che impediscono al mio script Python di essere veloce come un programma C ++ equivalente?


2
Sì, può. Vedi PyPy per lo stato dell'arte nei compilatori Python.
Greg Hewgill,

5
Tutte le variabili in Python sono polimorfiche, il che significa che il tipo di variabile è noto solo in fase di esecuzione. Se vedi (assumendo numeri interi) x + y in linguaggi simili a C, fanno un'aggiunta intera. In python ci sarà un interruttore sui tipi di variabili su xey e quindi verrà selezionata la funzione di aggiunta appropriata e poi ci sarà un controllo di overflow e poi ci sarà l'aggiunta. A meno che Python non impari a scrivere in modo statico questo overhead non sparirà mai.
nwp,

1
@nwp No, è facile, vedi PyPy. I problemi più complicati, ancora aperti, includono: Come superare la latenza di avvio dei compilatori JIT, come evitare allocazioni per grafici di oggetti di lunga durata complicati e come fare buon uso della cache in generale.

Risposte:


11

Ho colpito questo muro da solo quando ho preso un lavoro di programmazione Python a tempo pieno un paio di anni fa. Adoro Python, lo so davvero, ma quando ho iniziato a fare un tuning delle prestazioni, ho avuto degli shock maleducati.

I rigorosi pitonisti possono correggermi, ma ecco le cose che ho trovato, dipinte a tratti molto ampi.

  • L'uso della memoria di Python è abbastanza spaventoso. Python rappresenta tutto come un dict - che è estremamente potente, ma ha come risultato che anche i tipi di dati semplici sono giganteschi. Ricordo che il carattere "a" occupava 28 byte di memoria. Se stai utilizzando strutture di big data in Python, assicurati di fare affidamento su numpy o scipy, perché sono supportate dall'implementazione diretta di array di byte.

Ciò ha un impatto sulle prestazioni, perché significa che ci sono livelli extra di indiretta in fase di esecuzione, oltre a sfrecciare attorno a enormi quantità di memoria rispetto ad altre lingue.

  • Python ha un blocco dell'interprete globale, il che significa che per la maggior parte, i processi sono in esecuzione a thread singolo. Potrebbero esserci delle librerie che distribuiscono le attività tra i processi, ma stavamo creando circa 32 istanze del nostro script Python ed eseguendo ogni singolo thread.

Altri possono parlare con il modello di esecuzione, ma Python è una compilazione in fase di esecuzione e quindi interpretata, il che significa che non arriva fino al codice macchina. Anche questo ha un impatto sulle prestazioni. Puoi facilmente collegarti in moduli C o C ++, o trovarli, ma se esegui subito Python, avrà un impatto sulle prestazioni.

Ora, nei benchmark dei servizi Web, Python si confronta favorevolmente con gli altri linguaggi di compilazione in fase di runtime come Ruby o PHP. Ma è piuttosto indietro rispetto alla maggior parte dei linguaggi compilati. Anche i linguaggi che vengono compilati in un linguaggio intermedio ed eseguiti in una macchina virtuale (come Java o C #) fanno molto, molto meglio.

Ecco una serie davvero interessante di test di riferimento a cui mi riferisco di tanto in tanto:

http://www.techempower.com/benchmarks/

(Detto questo, adoro ancora Python, e se avrò la possibilità di scegliere la lingua in cui sto lavorando, è la mia prima scelta. Il più delle volte, non sono vincolato dai pazzi requisiti di throughput comunque.)


2
La stringa "a" non è un buon esempio per il primo punto elenco. Una stringa Java ha anche un notevole sovraccarico per le stringhe a singolo carattere, ma è un sovraccarico costante che si ammortizza abbastanza bene man mano che la stringa aumenta di lunghezza (da uno a quattro byte su caratteri a seconda della versione, delle opzioni di costruzione e del contenuto della stringa). Hai ragione sugli oggetti definiti dall'utente, almeno quelli che non usano __slots__. PyPy dovrebbe fare molto meglio al riguardo, ma non so abbastanza per giudicare.

1
Il secondo problema che stai segnalando è legato solo all'implementazione specifica e non inerente al linguaggio. Il primo problema richiede una spiegazione: ciò che "pesa" 28 byte non è il carattere stesso, ma il fatto che è stato impacchettato in una classe di stringhe, con i suoi metodi e proprietà. Rappresentare un singolo carattere come array di byte (letteralmente b'a ') "solo" pesa 18 byte su Python 3.3 e sono sicuro che ci sono altri modi per ottimizzare la memorizzazione dei caratteri in memoria se la tua applicazione ne ha davvero bisogno.
Red

C # può essere compilato in modo nativo (ad es. Tecnologia MS in arrivo, Xamarin per iOS).
Den,

13

L'implementazione di riferimento di Python è l'interprete "CPython". Cerca di essere ragionevolmente veloce, ma attualmente non utilizza ottimizzazioni avanzate. E per molti scenari di utilizzo, questa è una buona cosa: la compilazione in un codice intermedio avviene immediatamente prima del runtime e ogni volta che il programma viene eseguito il codice viene compilato di nuovo. Quindi il tempo necessario per l'ottimizzazione deve essere valutato rispetto al tempo guadagnato dalle ottimizzazioni - se non c'è un guadagno netto, l'ottimizzazione è inutile. Per un programma di lunga durata, o un programma con loop molto stretti, sarebbe utile utilizzare ottimizzazioni avanzate. Tuttavia, CPython viene utilizzato per alcuni lavori che precludono l'ottimizzazione aggressiva:

  • Script a esecuzione breve, utilizzati ad es. Per attività sysadmin. Molti sistemi operativi come Ubuntu costruiscono buona parte della loro infrastruttura su Python: CPython è abbastanza veloce per il lavoro, ma praticamente non ha tempi di avvio. Finché è più veloce di bash, è buono.

  • CPython deve avere una semantica chiara, in quanto è un'implementazione di riferimento. Ciò consente semplici ottimizzazioni come "ottimizza l'implementazione dell'operatore foo" o "compila le comprensioni dell'elenco in modo da codificare per codice più veloce", ma generalmente impedirà le ottimizzazioni che distruggono le informazioni, come le funzioni incorporate.

Naturalmente, ci sono più implementazioni Python oltre a CPython:

  • Jython è costruito sulla parte superiore della JVM. JVM è in grado di interpretare o compilare JIT il bytecode fornito e ha ottimizzazioni guidate dal profilo. Soffre di tempi di avvio elevati e ci vuole un po 'prima che la JIT entri in azione.

  • PyPy è uno stato dell'arte, JITting Python VM. PyPy è scritto in RPython, un sottoinsieme limitato di Python. Questo sottoinsieme rimuove un po 'di espressività da Python, ma consente di inferire staticamente il tipo di qualsiasi variabile. La VM scritta in RPython può quindi essere traspilata in C, il che offre prestazioni simili a RPython C. Tuttavia, RPython è ancora più espressivo di C, il che consente uno sviluppo più rapido di nuove ottimizzazioni. PyPy è un esempio di bootstrap del compilatore. PyPy (non RPython!) È principalmente compatibile con l'implementazione di riferimento di CPython.

  • Cython è (come RPython) un dialetto Python incompatibile con la tipizzazione statica. Traspila anche al codice C ed è in grado di generare facilmente estensioni C per l'interprete CPython.

Se sei disposto a tradurre il tuo codice Python in Cython o RPython, otterrai prestazioni di tipo C. Tuttavia, non dovrebbero essere intesi come "un sottoinsieme di Python", ma piuttosto come "C con sintassi Pythonic". Se passi a PyPy, il tuo codice Python vaniglia avrà un notevole aumento di velocità, ma non sarà in grado di interfacciarsi con estensioni scritte in C o C ++.

Ma quali proprietà o caratteristiche impediscono a Python vaniglia di raggiungere livelli di prestazioni simili a quelli della C, oltre a lunghi tempi di avvio?

  • Collaboratori e finanziamenti. A differenza di Java o C #, non esiste una singola società di guida dietro la lingua con un interesse a rendere questa lingua il migliore della sua categoria. Ciò limita lo sviluppo principalmente ai volontari e alle sovvenzioni occasionali.

  • Rilegatura tardiva e mancanza di digitazione statica. Python ci permette di scrivere schifezze in questo modo:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding
    

    In Python, qualsiasi variabile può essere riassegnata in qualsiasi momento. Questo evita la memorizzazione nella cache o l'inline; qualsiasi accesso deve passare attraverso la variabile. Questa indiretta appesantisce le prestazioni. Naturalmente: se il tuo codice non fa cose così folli in modo che a ciascuna variabile possa essere assegnato un tipo definitivo prima della compilazione e ogni variabile sia assegnata una sola volta, allora - in teoria - si potrebbe scegliere un modello di esecuzione più efficiente. Un linguaggio con questo in mente fornirebbe un modo per contrassegnare gli identificatori come costanti e almeno consentirebbe annotazioni di tipo opzionali ("digitazione graduale").

  • Un modello di oggetto discutibile. A meno che non vengano utilizzati gli slot, è difficile capire quali campi ha un oggetto (un oggetto Python è essenzialmente una tabella di campi hash). E anche una volta che siamo lì, non abbiamo ancora idea di quali tipi abbiano questi campi. Ciò impedisce di rappresentare gli oggetti come strutture strettamente compattate, come nel caso di C ++. (Naturalmente, la rappresentazione degli oggetti in C ++ non è l'ideale neanche: a causa della natura simile alla struttura, anche i campi privati ​​appartengono all'interfaccia pubblica di un oggetto.)

  • Raccolta dei rifiuti. In molti casi, GC potrebbe essere evitato completamente. C ++ ci permette di allocare staticamente oggetti che vengono distrutti automaticamente quando l'ambito corrente rimane: Type instance(args);. Fino ad allora, l'oggetto è vivo e può essere prestato ad altre funzioni. Questo di solito viene fatto tramite "pass-by-reference". Linguaggi come Rust consentono al compilatore di verificare staticamente che nessun puntatore a tale oggetto superi la durata dell'oggetto. Questo schema di gestione della memoria è totalmente prevedibile, altamente efficiente e si adatta alla maggior parte dei casi senza grafici di oggetti complicati. Sfortunatamente, Python non è stato progettato pensando alla gestione della memoria. In teoria, l'analisi di fuga può essere utilizzata per trovare casi in cui GC può essere evitato. In pratica, semplici catene di metodi comefoo().bar().baz() dovrà allocare un gran numero di oggetti di breve durata sull'heap (GC generazionale è un modo per mantenere piccolo questo problema).

    In altri casi, il programmatore potrebbe già conoscere la dimensione finale di alcuni oggetti come un elenco. Sfortunatamente, Python non offre un modo per comunicare questo quando si crea un nuovo elenco. Invece, i nuovi elementi verranno spinti alla fine, il che potrebbe richiedere diverse riallocazioni. Alcune note:

    • Gli elenchi di una dimensione specifica possono essere creati come fixed_size = [None] * size. Tuttavia, la memoria per gli oggetti all'interno di tale elenco dovrà essere allocata separatamente. Contrasto C ++, dove possiamo fare std::array<Type, size> fixed_size.

    • Gli array compressi di un tipo nativo specifico possono essere creati in Python tramite il arraymodulo integrato. Inoltre, numpyoffre rappresentazioni efficienti di buffer di dati con forme specifiche per tipi numerici nativi.

Sommario

Python è stato progettato per facilità d'uso, non per prestazioni. Il suo design rende piuttosto difficile la realizzazione di implementazioni altamente efficienti. Se il programmatore si astiene da caratteristiche problematiche, un compilatore che comprende gli idiomi rimanenti sarà in grado di emettere codice efficiente che può competere con C in termini di prestazioni.


8

Sì. Il problema principale è che la lingua è definita dinamica, ovvero non sai mai cosa stai facendo finché non lo stai per fare. Questo rende molto difficile per la produzione di codice macchina efficiente, perché non si sa cosa codice macchina prodotti per . I compilatori JIT possono fare un po 'di lavoro in quest'area, ma non è mai paragonabile a C ++ perché il compilatore JIT semplicemente non può passare il tempo e la memoria in esecuzione, poiché è il tempo e la memoria che non stai spendendo per eseguire il tuo programma e ci sono limiti duri su cosa possono raggiungere senza interrompere la semantica del linguaggio dinamico.

Non ho intenzione di affermare che questo è un compromesso inaccettabile. Ma è fondamentale per la natura di Python che le implementazioni reali non saranno mai così veloci come le implementazioni in C ++.


8

Ci sono tre fattori principali che influenzano le prestazioni di tutti i linguaggi dinamici, alcuni più di altri.

  1. Spese generali interpretative. In fase di runtime esiste un qualche tipo di codice byte anziché istruzioni macchina e c'è un overhead fisso per l'esecuzione di questo codice.
  2. Spese generali di spedizione. Il target per una chiamata di funzione non è noto fino al runtime e scoprire quale metodo chiamare richiede un costo.
  3. Overhead di gestione della memoria. I linguaggi dinamici memorizzano elementi in oggetti che devono essere allocati e deallocati e che comportano un sovraccarico di prestazioni.

Per C / C ++ i costi relativi di questi 3 fattori sono quasi zero. Le istruzioni vengono eseguite direttamente dal processore, la spedizione richiede al massimo una o due direzioni, la memoria heap non viene mai allocata a meno che non lo si dica. Un codice ben scritto può avvicinarsi al linguaggio assembly.

Per C # / Java con compilation JIT i primi due sono bassi ma la memoria raccolta dati inutili ha un costo. Il codice ben scritto può avvicinarsi a 2x C / C ++.

Per Python / Ruby / Perl il costo di tutti e tre questi fattori è relativamente alto. Pensa 5 volte rispetto a C / C ++ o peggio. (*)

Ricorda che il codice della libreria di runtime potrebbe essere scritto nella stessa lingua dei tuoi programmi e avere le stesse limitazioni delle prestazioni.


(*) Poiché la compilazione di Just-In_Time (JIT) è estesa a questi linguaggi, anche loro si avvicineranno (tipicamente a 2x) alla velocità del codice C / C ++ ben scritto.

Va anche notato che una volta che il divario è ridotto (tra linguaggi concorrenti), le differenze sono dominate da algoritmi e dettagli di implementazione. Il codice JIT può battere C / C ++ e C / C ++ può battere il linguaggio assembly perché è più semplice scrivere un buon codice.


"Ricorda che il codice della libreria di runtime potrebbe essere scritto nella stessa lingua dei tuoi programmi e avere le stesse limitazioni delle prestazioni." e "Per Python / Ruby / Perl il costo di tutti e tre questi fattori è relativamente alto. Pensa 5 volte rispetto a C / C ++ o peggio." In realtà, questo non è vero. Ad esempio, la Hashclasse Rubinius (una delle principali strutture di dati in Ruby) è scritta in Ruby, e si comporta in modo comparabile, a volte anche più veloce, della Hashclasse YARV che è scritta in C. E uno dei motivi è che gran parte del runtime di Rubinius il sistema è scritto in Ruby, in modo che possano ...
Jörg W Mittag il

... ad esempio essere sottolineato dal compilatore Rubinius. Esempi estremi sono la VM Klein (una VM metacircolare per Self) e la VM Maxine (una VM metacircolare per Java), dove tutto , anche il codice di invio del metodo, il garbage collector, l'allocatore di memoria, i tipi primitivi, le strutture di dati di base e gli algoritmi sono scritti in Self o Java. In questo modo, anche parti della VM principale possono essere integrate nel codice utente e la VM può ricompilare e riottimizzare se stessa utilizzando il feedback di runtime dal programma utente.
Jörg W Mittag,

@ JörgWMittag: ancora vero. Rubinius ha JIT e il codice JIT batte spesso C / C ++ sui singoli benchmark. Non riesco a trovare alcuna prova che questa roba metacircolare faccia molto per la velocità in assenza di JIT. [Vedi modifica per chiarezza su JIT.]
david.pfx

1

Ma ci sono limiti tecnici o funzionalità del linguaggio che impediscono al mio script Python di essere veloce come un programma C ++ equivalente?

No. È solo una questione di soldi e risorse impiegate per far funzionare il C ++ velocemente contro denaro e risorse investite per far funzionare Python velocemente.

Ad esempio, quando uscì il Self VM, non era solo il linguaggio OO dinamico più veloce, era anche il periodo linguistico OO più veloce. Pur essendo un linguaggio incredibilmente dinamico (molto più di Python, Ruby, PHP o JavaScript, ad esempio), è stato più veloce della maggior parte delle implementazioni C ++ disponibili.

Ma poi Sun ha annullato il progetto Self (un linguaggio OO maturo per scopi generali per lo sviluppo di sistemi di grandi dimensioni) per concentrarsi su un piccolo linguaggio di scripting per menu animati in set top box TV (potresti averne sentito parlare, si chiama Java), non c'era più finanziamenti. Allo stesso tempo, Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. speso ingenti somme di denaro e risorse per velocizzare il C ++. I produttori di CPU hanno aggiunto funzionalità ai loro chip per rendere veloce il C ++. I sistemi operativi sono stati scritti o modificati per rendere veloce il C ++. Quindi, C ++ è veloce.

Non ho una grande familiarità con Python, sono più una persona Ruby, quindi darò un esempio da Ruby: la Hashclasse (equivalente in funzione e importanza dictin Python) nell'implementazione di Rubinius Ruby è scritta in Ruby puro al 100%; tuttavia compete in modo favorevole e talvolta supera anche la Hashclasse in YARV che è scritta in C. ottimizzata a mano. E rispetto ad alcuni dei sistemi commerciali Lisp o Smalltalk (o la suddetta Self VM), il compilatore di Rubinius non è nemmeno così intelligente .

Non c'è nulla di inerente in Python che lo rallenta. Ci sono funzionalità nei processori e nei sistemi operativi odierni che danneggiano Python (ad esempio, la memoria virtuale è nota per essere terribile per le prestazioni della garbage collection). Ci sono funzionalità che aiutano C ++ ma non aiutano Python (le moderne CPU cercano di evitare i problemi di cache, perché sono così costosi. Sfortunatamente, evitare i problemi di cache è difficile quando si hanno OO e polimorfismo. Piuttosto, si dovrebbe ridurre il costo della cache manca. La CPU Azul Vega, progettata per Java, lo fa.)

Se si spendono tanti soldi, ricerca e risorse per rendere Python veloce, come è stato fatto per C ++, e si spendono tanti soldi, ricerca e risorse per creare sistemi operativi che fanno funzionare i programmi Python velocemente come è stato fatto per C ++ e si spende come molti soldi, ricerche e risorse per realizzare CPU che fanno funzionare velocemente i programmi Python come è stato fatto per C ++, quindi non ho dubbi che Python possa raggiungere prestazioni paragonabili a C ++.

Abbiamo visto con ECMAScript cosa può succedere se solo un giocatore prende sul serio le prestazioni. Nel giro di un anno, abbiamo avuto sostanzialmente un aumento delle prestazioni di 10 volte su tutta la linea per tutti i principali fornitori.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.