Come funziona la garbage collection in lingue compilate in modo nativo?


79

Dopo aver sfogliato diverse risposte in Stack Overflow, è chiaro che alcune lingue compilate in modo nativo hanno la garbage collection . Ma non mi è chiaro come funzionerebbe esattamente.

Capisco come la garbage collection potrebbe funzionare con un linguaggio interpretato. Il garbage collector correva semplicemente accanto all'interprete ed eliminava oggetti inutilizzati e non raggiungibili dalla memoria del programma. Entrambi corrono insieme.

Come funzionerebbe con le lingue compilate? La mia comprensione è che una volta che il compilatore ha compilato il codice sorgente nel codice di destinazione - in particolare il codice macchina nativo - è fatto. Il suo lavoro è finito. Quindi, come è possibile raccogliere anche la spazzatura nel programma compilato?

Il compilatore funziona in qualche modo con la CPU mentre il programma viene eseguito per eliminare oggetti "spazzatura"? Oppure il compilatore include alcuni garbage collector minimi nell'eseguibile del programma compilato.

Credo che la mia ultima affermazione avrebbe più validità della prima a causa di questo estratto da questa risposta su Stack Overflow :

Uno di questi linguaggi di programmazione è Eiffel. La maggior parte dei compilatori Eiffel generano codice C per motivi di portabilità. Questo codice C viene utilizzato per produrre il codice macchina da un compilatore C standard. Le implementazioni Eiffel forniscono GC (e talvolta anche GC accurati) per questo codice compilato e non è necessario VM. In particolare, il compilatore VisualEiffel ha generato il codice macchina x86 nativo direttamente con il supporto GC completo .

L'ultima affermazione sembra implicare che il compilatore includa un programma nell'eseguibile finale che funge da garbage collector mentre il programma è in esecuzione.

La pagina sul sito Web del linguaggio D sulla garbage collection - che è compilata in modo nativo e ha un garbage collector opzionale - sembra anche suggerire che alcuni programmi in background vengono eseguiti insieme al programma eseguibile originale per implementare la garbage collection.

D è un linguaggio di programmazione di sistemi con supporto per la garbage collection. Di solito non è necessario liberare memoria esplicitamente. Basta allocare secondo necessità e il garbage collector restituirà periodicamente tutta la memoria inutilizzata al pool di memoria disponibile.

Se viene utilizzato il metodo sopra menzionato , come funzionerebbe esattamente? Il compilatore memorizza una copia di alcuni programmi di garbage collection e li incolla in ogni eseguibile che genera?

O sono imperfetto nel mio pensiero? In tal caso, quali metodi vengono utilizzati per implementare la garbage collection per le lingue compilate e come funzionano esattamente?


1
Gradirei se il vicino elettore di questa domanda potesse affermare esattamente cosa non va e potrei risolverlo?
Christian Dean,

6
Se si accetta il fatto che il GC è fondamentalmente parte di una libreria richiesta da una particolare implementazione del linguaggio di programmazione, l'essenza della domanda non ha nulla a che fare con GC di per sé e tutto ciò che riguarda il collegamento statico contro dinamico .
Theodoros Chatzigiannakis,

7
È possibile considerare Garbage Collector come parte della libreria di runtime che implementa l'equivalente del linguaggio malloc().
Barmar,

9
Il funzionamento di un garbage collector dipende dalle caratteristiche del allocatore , non il modello di compilazione . L'allocatore conosce ogni oggetto che è stato allocato; li ha assegnati. Ora tutto ciò che serve è un modo per sapere quali oggetti sono ancora vivi e il collezionista può deallocare tutti gli oggetti tranne loro. Nulla in quella descrizione ha nulla a che fare con il modello di compilazione.
Eric Lippert,

1
GC è una caratteristica della memoria dinamica, non una caratteristica dell'interprete.
Dmitry Grigoryev,

Risposte:


52

La garbage collection in una lingua compilata funziona allo stesso modo di una lingua interpretata. Lingue come Go usano la traccia dei garbage collector anche se il loro codice viene solitamente compilato in anticipo per il codice macchina.

(Tracciamento) La garbage collection di solito inizia percorrendo le pile di chiamate di tutti i thread attualmente in esecuzione. Gli oggetti su quelle pile sono sempre vivi. Successivamente, il Garbage Collector attraversa tutti gli oggetti a cui sono puntati oggetti vivi, fino a quando non viene scoperto l'intero grafico degli oggetti vivi.

È chiaro che farlo richiede informazioni extra che lingue come C non forniscono. In particolare, richiede una mappa del frame dello stack di ciascuna funzione che contenga gli offset di tutti i puntatori (e probabilmente i relativi tipi di dati) nonché le mappe di tutti i layout degli oggetti che contengono le stesse informazioni.

È comunque facile vedere che i linguaggi che hanno garanzie di tipo forte (ad esempio se non sono consentiti cast di puntatori a tipi di dati diversi) possono effettivamente calcolare quelle mappe in fase di compilazione. Memorizzano semplicemente un'associazione tra indirizzi di istruzione e mappe di stack frame e un'associazione tra tipi di dati e mappe di layout degli oggetti all'interno del binario. Queste informazioni consentono quindi di eseguire l'attraversamento del grafico a oggetti.

Il Garbage Collector stesso non è altro che una libreria collegata al programma, simile alla libreria C standard. Ad esempio, questa libreria potrebbe fornire una funzione simile a malloc()quella che esegue l'algoritmo di raccolta se la pressione della memoria è alta.


9
Tra le librerie di utilità e la compilazione JIT, le linee tra "compilato in nativo" e "corre in un ambiente di runtime" stanno diventando sempre più sfocate.
corsiKa

6
Solo per aggiungere un po 'di lingue che non vengono fornite con il supporto GC: È vero che C e altre lingue simili non forniscono informazioni sugli stack di chiamate, ma se stai bene con un codice specifico della piattaforma (di solito incluso un po' del codice assembly) è ancora possibile implementare la "garbage collection conservativa". Il Boehm GC è un esempio di questo utilizzato nei programmi di vita reale.
Matti Virkkunen,

2
@corsiKa O meglio, la linea è molto più distinta. Ora vediamo che quelli sono concetti non correlati diversi e non contrari l'uno all'altro.
Kroltan,

4
Un'ulteriore complessità di cui devi essere consapevole nei runtime compilati e interpretati si riferisce a questa frase nella tua risposta: "La garbage collection (di tracciamento) di solito inizia camminando le pile di chiamate di tutti i thread attualmente in esecuzione". La mia esperienza di implementazione di GC in un ambiente compilato è che tracciare le pile non è sufficiente. Il punto di partenza di solito è sospendere i thread per il tempo necessario a rintracciarli dai loro registri , perché potrebbero avere riferimenti in quei registri che non sono stati ancora memorizzati nello stack. Per un interprete, questo di solito non è ...
Jules,

... un problema, perché l'ambiente può disporre che GC abbia luogo in "punti sicuri" in cui l'interprete sa che tutti i dati sono memorizzati in modo sicuro negli stack interpretati.
Jules,

123

Il compilatore memorizza una copia di alcuni programmi di garbage collection e li incolla in ogni eseguibile che genera?

Sembra poco elegante e strano, ma sì. Il compilatore ha un'intera libreria di utilità, contenente molto più di un semplice codice garbage collection e le chiamate a questa libreria verranno inserite in ogni eseguibile che crea. Questa si chiama libreria di runtime e rimarrai sorpreso da quante diverse attività in genere svolge.


51
@ChristianDean Nota che anche C ha una libreria di runtime. Sebbene non abbia GC, esegue comunque la gestione della memoria attraverso quella libreria di runtime: malloc()e free()non sono integrati nella lingua, non fanno parte del sistema operativo, ma sono funzioni in questa libreria. Il C ++ viene talvolta compilato con una libreria di garbage collection, anche se il linguaggio non è stato progettato pensando a GC.
Amon,

18
C ++ contiene anche una libreria di runtime che fa funzionare cose come make dynamic_casted eccezioni, anche se non aggiungi un GC.
Sebastian Redl,

23
La libreria di runtime non viene necessariamente copiata in ciascun eseguibile (che si chiama collegamento statico) a cui può solo fare riferimento (un percorso del binario contenente la libreria) e accessibile al momento dell'esecuzione: si tratta di un collegamento dinamico.
mouviciel,

16
Inoltre, il compilatore non è tenuto a saltare direttamente al punto di accesso del programma senza che accada altro. Sto facendo un'ipotesi plausibile sul fatto che ogni compilatore inserisca effettivamente un gruppo di codice di inizializzazione specifico della piattaforma prima di chiamare main(), ed è perfettamente legale, diciamo, accendere un thread GC in questo codice. (Supponendo che il GC non venga eseguito all'interno delle chiamate di allocazione della memoria.) In fase di esecuzione, GC deve solo sapere quali parti di un oggetto sono puntatori o riferimenti a oggetti e il compilatore deve emettere il codice per tradurre un riferimento a un puntatore se il GC trasferisce gli oggetti.
millimoose,

15
@millimoose: Sì. Ad esempio, su GCC, questo pezzo di codice è crt0.o(che sta per " C R un T ime, le basi"), che viene collegato a tutti i programmi (o almeno a tutti i programmi che non sono indipendenti ).
Jörg W Mittag,

58

Oppure il compilatore include alcuni garbage collector minimi nel codice del programma compilato.

È un modo strano di dire "il compilatore collega il programma con una libreria che esegue la garbage collection". Ma sì, è quello che sta succedendo.

Questo non è niente di speciale: i compilatori di solito collegano tonnellate di librerie ai programmi che compilano; altrimenti i programmi compilati non potrebbero fare molto senza implementare molte cose da zero: anche scrivere testo sullo schermo / un file / ... richiede una libreria.

Ma forse GC è diverso da queste altre librerie, che forniscono API esplicite che l'utente chiama?

No: nella maggior parte dei linguaggi, le librerie di runtime svolgono molto lavoro dietro le quinte senza API rivolte al pubblico, oltre GC. Considera questi tre esempi:

  1. Propagazione delle eccezioni e stack svolgendo / chiamata distruttore.
  2. Allocazione dinamica della memoria (che di solito non sta solo chiamando una funzione, come in C, anche quando non c'è garbage collection).
  3. Tracciamento di informazioni di tipo dinamico (per cast, ecc.).

Quindi una libreria di garbage collection non è affatto speciale e a priori non ha nulla a che fare con se un programma è stato compilato in anticipo.


questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nella risposta più importante pubblicata 3 ore prima
moscerino

11
@gnat Mi è sembrato utile / necessario perché la risposta migliore non è di gran lunga abbastanza forte: menziona fatti simili, ma non sottolinea che individuare la raccolta dei rifiuti è una distinzione completamente artificiale. Fondamentalmente, l'ipotesi di OP è errata e la risposta principale non menziona questo. Il mio lo fa (evitando il termine piuttosto brusco "imperfetto").
Konrad Rudolph,

Non è poi così speciale, ma direi che è in qualche modo speciale, dal momento che di solito le persone pensano alle biblioteche come qualcosa che chiamano esplicitamente dal loro codice; piuttosto che un'implementazione della semantica del linguaggio fondamentale. Penso che l'ipotesi sbagliata dell'OP qui sia piuttosto che un compilatore debba solo tradurre il codice in un modo più o meno semplice, piuttosto che strumentarlo con chiamate in libreria che l'autore non ha specificato.
millimoose,

7
Le librerie di runtime di @millimoose funzionano dietro le quinte in una moltitudine di modi senza l'interazione esplicita dell'utente. Considera la propagazione delle eccezioni e impila le chiamate svolgitore / distruttore. Prendi in considerazione l'allocazione dinamica della memoria (che di solito non sta semplicemente chiamando una funzione, come in C, anche quando non è presente la garbage collection). Prendi in considerazione la gestione di informazioni di tipo dinamico (per cast, ecc.). Quindi il GC non è davvero unico.
Konrad Rudolph,

3
Sì, ammetto di averlo scritto in modo strano. Questo semplicemente perché ero scettico sul fatto che il compilatore stesse facendo qualcosa del genere. Ma ora che ci penso, ha molto più senso. Il compilatore potrebbe semplicemente collegare un garbage collector come qualsiasi altra parte della libreria standard. Credo che parte della mia confusione derivasse dal pensare a un garbage collector solo come parte dell'implementazione di un interprete e non come un programma separato a sé stante.
Christian Dean,

23

Come funzionerebbe con le lingue compilate?

La tua formulazione è sbagliata. Un linguaggio di programmazione è una specifica scritta in alcuni rapporti tecnici (per un buon esempio, vedere R5RS ). In realtà ti stai riferendo ad un'implementazione di un linguaggio specifico (che è un software).

(alcuni linguaggi di programmazione hanno caratteristiche cattivi, o anche quelle mancanti, o semplicemente conforme a qualche esempio di implementazione, ancora, un linguaggio di programmazione definisce un comportamento - ad esempio, ha una sintassi e la semantica -, è non è un prodotto software, ma potrebbe essere implementato da alcuni prodotti software; molti linguaggi di programmazione hanno diverse implementazioni; in particolare, "compilato" è un aggettivo che si applica alle implementazioni - anche se alcuni linguaggi di programmazione sono implementati più facilmente dagli interpreti che dai compilatori.)

La mia comprensione è che una volta che il compilatore ha compilato il codice sorgente nel codice di destinazione - in particolare il codice macchina nativo - è fatto. Il suo lavoro è finito.

Si noti che interpreti e compilatori hanno un significato ampio e che alcune implementazioni linguistiche potrebbero essere considerate entrambe. In altre parole, c'è un continuum nel mezzo. Leggi l'ultimo Dragon Book e pensa al bytecode , alla compilazione JIT , che emette in modo dinamico il codice C che viene compilato in alcuni "plugin", quindi dlopen (3) -ed dallo stesso processo (e su macchine attuali, questo è abbastanza veloce per essere compatibile con un REPL interattivo , vedi questo )


Consiglio vivamente di leggere il manuale GC . È necessario un intero libro per rispondere . Prima di ciò, leggi il wikipage della Garbage Collection (che presumo tu abbia letto prima di leggere di seguito).

Il sistema di runtime dell'implementazione del linguaggio compilato contiene il garbage collector e il compilatore sta generando codice adatto a quel particolare sistema di runtime. In particolare, le primitive di allocazione (sono compilate in codice macchina che) chiameranno (o potrebbero) il sistema di runtime.

Quindi, come potrebbe anche essere raccolta spazzatura il programma compilato?

Emettendo semplicemente il codice macchina che utilizza (ed è "amichevole" e "compatibile con") il sistema di runtime.

Nota che puoi trovare diverse librerie di garbage collection, in particolare Boehm GC , l'MPS di Ravenbrook o persino il mio (non mantenuto) Qish . E codificare un GC semplice non è molto difficile (tuttavia, il debug è più difficile e codificare un GC competitivo è difficile ).

In alcuni casi, il compilatore userebbe un GC conservativo (come Boehm GC ). Quindi, non c'è molto da codificare. Il GC conservativo (quando il compilatore chiama la sua routine di allocazione o l'intera routine GC) a volte scansiona l'intero stack di chiamate e presume che qualsiasi zona di memoria (indirettamente) raggiungibile dallo stack di chiamate sia attiva. Questo viene chiamato GC conservativo perché le informazioni di digitazione vengono perse: se un numero intero nello stack di chiamate sembra un indirizzo, verrà seguito, ecc.

In altri casi (più difficili), il runtime fornisce una garbage collection di copia generazionale (un esempio tipico è il compilatore Ocaml, che compila il codice Ocaml in codice macchina usando un tale GC). Quindi il problema è trovare precisamente sulla chiamata che impila tutti i puntatori e alcuni di essi vengono spostati dal GC. Quindi il compilatore genera metadati che descrivono i frame dello stack di chiamate, utilizzati dal runtime. Quindi le convenzioni di chiamata e l' ABI stanno diventando specifici per l'implementazione (cioè il compilatore) e il sistema di runtime.

In alcuni casi, il codice macchina generato dal compilatore (in realtà anche chiusure che lo puntano) è esso stesso spazzatura raccolta . Questo è in particolare il caso di SBCL (una buona implementazione di Common Lisp) che genera codice macchina per ogni interazione REPL . Ciò richiede anche alcuni metadati che descrivono il codice e i frame di chiamata utilizzati al suo interno.

Il compilatore memorizza una copia di alcuni programmi di garbage collection e li incolla in ogni eseguibile che genera?

Una specie di. Tuttavia, il sistema di runtime potrebbe essere una libreria condivisa, ecc. Talvolta (su Linux e molti altri sistemi POSIX) potrebbe persino essere un interprete di script, ad esempio passato a execve (2) con un shebang . O un interprete ELF , vedi elf (5) e PT_INTERP, ecc.

A proposito, la maggior parte dei compilatori per il linguaggio con Garbage Collection (e il loro sistema di runtime) sono oggi software gratuiti . Quindi scarica il codice sorgente e studialo.


5
Intendi dire che esistono molte implementazioni del linguaggio di programmazione senza una specifica esplicita. Sì, sono d'accordo. Ma il mio punto è che un linguaggio di programmazione non è un software (come un compilatore o un interprete). È qualcosa che ha una sintassi e una semantica (forse entrambi mal definiti).
Basile Starynkevitch,

4
@KonradRudolph: dipende interamente dalla definizione di "formale" e "specifica" :-D Esiste la specifica del linguaggio di programmazione ISO / IEC 30170: 2012 Ruby , che specifica un piccolo sottoinsieme dell'intersezione di Ruby 1.8 e 1.9. C'è la Ruby Spec Suite , un insieme di esempi di casi limite che servono come una sorta di "specifica eseguibile". Quindi, The Ruby Programming Language di David Flanagan e Yukihiro Matsumoto .
Jörg W Mittag,

4
Inoltre, la documentazione di Ruby . Discussioni di problemi sul Ruby Issue Tracker . Discussioni sulle mailing list ruby-core (inglese) e ruby-dev (giapponese). Le aspettative di buon senso della comunità (ad es. Nel Array#[]caso peggiore O (1), nel caso peggiore Hash#[]è O (1) ammortizzato). E ultimo ma non meno importante: il cervello di Matz.
Jörg W Mittag,

6
@KonradRudolph: Il punto è: anche un linguaggio senza specifiche formali e solo una singola implementazione può ancora essere separato in "il linguaggio" (le regole e le restrizioni astratte) e "l'implementazione" (i programmi di elaborazione del codice secondo tali regole e restrizioni). E l'implementazione genera ancora una specifica, sebbene banale, vale a dire: "qualunque cosa faccia il codice è la specifica". È così che sono state scritte le specifiche ISO, RubySpec e RDocs, dopotutto: giocando con WRIth e / o reverse engineering MRI.
Jörg W Mittag,

1
Sono contento che tu abbia allevato il bidone della spazzatura di Bohem. Consiglierei all'OP di studiarlo perché è un eccellente esempio di quanto possa essere semplice la garbage collection, anche se "fissata" a un compilatore esistente.
Cort Ammon,

6

Ci sono già alcune buone risposte, ma vorrei chiarire alcuni malintesi dietro questa domanda.

Di per sé non esiste un "linguaggio compilato in modo nativo". Ad esempio, lo stesso codice Java è stato interpretato (e quindi parzialmente compilato just-in-time in fase di esecuzione) sul mio vecchio telefono (Java Dalvik) ed è (in anticipo) compilato sul mio nuovo telefono (ART).

La differenza tra eseguire il codice in modo nativo e interpretato è molto meno severa di quanto sembri. Entrambi hanno bisogno di alcune librerie di runtime e di alcuni sistemi operativi per funzionare (*). Il codice interpretato richiede un interprete, ma l'interprete è solo una parte del runtime. Ma anche questo non è rigoroso, in quanto potresti sostituire l'interprete con un compilatore (just-in-time). Per le massime prestazioni, potresti desiderare entrambi (il runtime Java desktop contiene un interprete e due compilatori).

Non importa come eseguire il codice, dovrebbe comportarsi allo stesso modo. Allocare e liberare memoria è un compito per il runtime (proprio come aprire file, avviare thread, ecc.). Nella tua lingua basta scrivere new X()o simili. La specifica della lingua dice cosa dovrebbe accadere e il runtime lo fa.

Viene allocata parte della memoria libera, viene richiamato il costruttore, ecc. Quando non c'è memoria sufficiente, viene chiamato il Garbage Collector. Dato che sei già in fase di esecuzione, che è un pezzo di codice nativo, l'esistenza di un interprete non ha alcuna importanza.

Non c'è davvero alcuna connessione diretta tra l'interpretazione del codice e la garbage collection. È solo che linguaggi di basso livello come C sono progettati per la velocità e il controllo accurato di tutto, il che non si adatta bene all'idea di codice non nativo o di un garbage collector. Quindi c'è solo una correlazione.

Questo era molto vero ai vecchi tempi, dove ad esempio l'interprete Java era molto lento e il garbage collector piuttosto inefficiente. Oggi le cose sono molto diverse e parlare di una lingua interpretata ha perso senso.


(*) Almeno quando si parla di codice generico, lasciando da parte i boot loader e simili.


Sia Ocaml che SBCL sono compilatori nativi. Quindi ci sono implementazioni di "linguaggio compilato in modo nativo".
Basile Starynkevitch,

@BasileStarynkevitch WAT? In che modo la denominazione di alcuni compilatori meno noti si collega alla mia risposta? SBCL come compilatore di un linguaggio interpretato in origine non è un argomento a favore della mia affermazione secondo cui la distinzione non ha senso?
Maaartinus,

Il Lisp comune (o qualsiasi altra lingua) non viene interpretato o compilato. È un linguaggio di programmazione (una specifica). La sua implementazione può essere un compilatore, un interprete o una via di mezzo (ad es. Un interprete bytecode). SBCL è un'implementazione compilata interattiva di Common Lisp. Ocaml è anche un linguaggio di programmazione (con implementazioni sia di un bytecode che di un compilatore nativo).
Basile Starynkevitch,

@BasileStarynkevitch Questo è quello che sto sostenendo. 1. Non esiste un linguaggio interpretato o compilato (sebbene C sia interpretato raramente e LISP fosse raramente compilato, ma questo non ha molta importanza). 2. Esistono implementazioni interpretate, compilate e miste per le lingue più conosciute e non esiste un linguaggio che precluda la compilazione o l'interpretazione.
Maaartinus,

6
Penso che la tua discussione abbia molto senso. Il punto chiave di Grok è che esegui sempre un "programma nativo", o "mai", comunque tu voglia vederlo. Nessun exe su Windows è di per sé eseguibile; ha bisogno di un caricatore e di altre funzionalità del sistema operativo solo per iniziare ed è anche parzialmente "interpretato". Questo diventa più ovvio con gli eseguibili .net. java myprogè tanto o poco nativo quanto grep myname /etc/passwdo ld.so myprog: è un eseguibile (qualunque cosa significhi) che accetta un argomento ed esegue operazioni con i dati.
Peter - Ripristina Monica il

3

I dettagli variano tra le implementazioni, ma generalmente sono alcune delle seguenti combinazioni:

  • Una libreria di runtime che include un GC. Questo gestirà l'allocazione della memoria e avrà alcuni altri punti di ingresso, inclusa una funzione "GC_now".
  • Il compilatore creerà tabelle per il GC in modo che sappia quali campi in quali tipi di dati sono riferimenti. Questo verrà fatto anche per i frame dello stack per ciascuna funzione in modo che il GC possa tracciarlo dallo stack.
  • Se il GC è incrementale (l'attività del GC è interfogliata con il programma) o simultanea (viene eseguita in un thread separato), il compilatore includerà anche un codice oggetto speciale per aggiornare le strutture di dati del GC quando vengono aggiornati i riferimenti. I due hanno problemi simili per la coerenza dei dati.

In GC incrementale e simultaneo il codice compilato e il GC devono cooperare per mantenere alcuni invarianti. Ad esempio, in un raccoglitore di copie il GC funziona copiando i dati in tempo reale dallo spazio A allo spazio B, lasciando dietro la spazzatura. Per il ciclo successivo capovolge A e B e si ripete. Quindi una regola può essere quella di garantire che ogni volta che il programma utente tenta di fare riferimento a un oggetto nello spazio A, questo viene rilevato e l'oggetto viene immediatamente copiato nello spazio B, dove il programma può continuare ad accedervi. Un indirizzo di inoltro viene lasciato nello spazio A per indicare al GC che ciò è accaduto in modo che eventuali altri riferimenti all'oggetto vengano aggiornati man mano che vengono tracciati. Questo è noto come "barriera di lettura".

Gli algoritmi GC sono stati studiati sin dagli anni '60 e la letteratura sull'argomento è ampia. Google se vuoi maggiori informazioni.

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.