while (1) vs. per (;;) C'è una differenza di velocità?


154

Versione lunga ...

Un collega ha affermato oggi dopo aver visto il mio uso while (1)in uno script Perl che for (;;)è più veloce. Ho sostenuto che dovrebbero essere le stesse sperando che l'interprete possa ottimizzare eventuali differenze. Ho impostato uno script che eseguiva 1.000.000.000 di iterazioni in loop e lo stesso numero di loop while e registra il tempo tra. Non ho trovato differenze apprezzabili. Il mio collega ha detto che un professore gli aveva detto che while (1)stava facendo un confronto 1 == 1e che non lo for (;;)era. Abbiamo ripetuto lo stesso test con 100 volte il numero di iterazioni con C ++ e la differenza era trascurabile. È stato tuttavia un esempio grafico di quanto può essere più veloce il codice compilato rispetto a un linguaggio di scripting.

Versione breve...

C'è qualche motivo per preferire un while (1)over a for (;;)se hai bisogno di un loop infinito per uscire?

Nota: se non è chiaro dalla domanda. Questa è stata puramente una divertente discussione accademica tra una coppia di amici. Sono consapevole che questo non è un concetto estremamente importante su cui tutti i programmatori dovrebbero essere angosciosi. Grazie per tutte le ottime risposte che io (e sono sicuro che altri) hanno imparato alcune cose da questa discussione.

Aggiornamento: il suddetto collega ha risposto con una risposta di seguito.

Citato qui nel caso venga sepolto.

Veniva da un programmatore di assemblaggio AMD. Ha dichiarato che i programmatori C (il popolo) non si rendono conto che il loro codice ha inefficienze. Ha detto oggi, comunque, che i compilatori gcc sono molto bravi e mettono fuori gioco persone come lui. Ha detto per esempio, e mi ha parlato del while 1vs for(;;). Lo uso ora per abitudine ma gcc e soprattutto gli interpreti faranno la stessa operazione (un salto del processore) per entrambi i giorni, poiché sono ottimizzati.


4
Sono curioso. Perché è necessario un ciclo infinito in uno script perl? Ovviamente non stai programmando un driver o una cosa di sistema ... L'infinito è abbastanza lungo :-)
Luc M

125
Quale ciclo infinito è il più veloce? LOL ... "Il mio nuovo computer è così veloce, esegue un ciclo infinito in poco meno di un'ora ..." ;-)
Arjan Einbu,

8
Era un professore di sociologia che glielo aveva detto? Nell'era moderna, il codice digitato non è quello che il computer finisce per vedere.
brian d foy,

5
mi aspetto che il tempo impiegato per testarlo sia molto più lungo del tempo potenzialmente risparmiato sapendo quale è più veloce, in entrambi i casi. anche se lo ammortizzi in entrambe le tue vite di programmazione.
Peter Recore,

4
Perché il compilatore dovrebbe mai generare codice per eseguire un test che non ha effetti collaterali e di cui conosce già il compilatore? Non ha senso.
David Schwartz,

Risposte:


218

In perl, generano gli stessi codici operativi:

$ perl -MO=Concise -e 'for(;;) { print "foo\n" }'
a  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 2 -e:1) v ->3
9     <2> leaveloop vK/2 ->a
3        <{> enterloop(next->8 last->9 redo->4) v ->4
-        <@> lineseq vK ->9
4           <;> nextstate(main 1 -e:1) v ->5
7           <@> print vK ->8
5              <0> pushmark s ->6
6              <$> const[PV "foo\n"] s ->7
8           <0> unstack v ->4
-e syntax OK

$ perl -MO=Concise -e 'while(1) { print "foo\n" }'
a  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 2 -e:1) v ->3
9     <2> leaveloop vK/2 ->a
3        <{> enterloop(next->8 last->9 redo->4) v ->4
-        <@> lineseq vK ->9
4           <;> nextstate(main 1 -e:1) v ->5
7           <@> print vK ->8
5              <0> pushmark s ->6
6              <$> const[PV "foo\n"] s ->7
8           <0> unstack v ->4
-e syntax OK

Allo stesso modo in GCC:

#include <stdio.h>

void t_while() {
    while(1)
        printf("foo\n");
}

void t_for() {
    for(;;)
        printf("foo\n");
}

    .file   "test.c"
    .section    .rodata
.LC0:
    .string "foo"
    .text
.globl t_while
    .type   t_while, @function
t_while:
.LFB2:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
.L2:
    movl    $.LC0, %edi
    call    puts
    jmp .L2
.LFE2:
    .size   t_while, .-t_while
.globl t_for
    .type   t_for, @function
t_for:
.LFB3:
    pushq   %rbp
.LCFI2:
    movq    %rsp, %rbp
.LCFI3:
.L5:
    movl    $.LC0, %edi
    call    puts
    jmp .L5
.LFE3:
    .size   t_for, .-t_for
    .section    .eh_frame,"a",@progbits
.Lframe1:
    .long   .LECIE1-.LSCIE1
.LSCIE1:
    .long   0x0
    .byte   0x1
    .string "zR"
    .uleb128 0x1
    .sleb128 -8
    .byte   0x10
    .uleb128 0x1
    .byte   0x3
    .byte   0xc
    .uleb128 0x7
    .uleb128 0x8
    .byte   0x90
    .uleb128 0x1
    .align 8
.LECIE1:
.LSFDE1:
    .long   .LEFDE1-.LASFDE1
.LASFDE1:
    .long   .LASFDE1-.Lframe1
    .long   .LFB2
    .long   .LFE2-.LFB2
    .uleb128 0x0
    .byte   0x4
    .long   .LCFI0-.LFB2
    .byte   0xe
    .uleb128 0x10
    .byte   0x86
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI1-.LCFI0
    .byte   0xd
    .uleb128 0x6
    .align 8
.LEFDE1:
.LSFDE3:
    .long   .LEFDE3-.LASFDE3
.LASFDE3:
    .long   .LASFDE3-.Lframe1
    .long   .LFB3
    .long   .LFE3-.LFB3
    .uleb128 0x0
    .byte   0x4
    .long   .LCFI2-.LFB3
    .byte   0xe
    .uleb128 0x10
    .byte   0x86
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI3-.LCFI2
    .byte   0xd
    .uleb128 0x6
    .align 8
.LEFDE3:
    .ident  "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
    .section    .note.GNU-stack,"",@progbits

Quindi immagino che la risposta sia la stessa in molti compilatori. Naturalmente, per alcuni altri compilatori questo potrebbe non essere necessariamente il caso, ma è probabile che il codice all'interno del loop sarà qualche migliaio di volte più costoso del loop stesso, quindi a chi importa?


15
prova con B :: Deparse, deparing un infinito per loop restituisce un ciclo while: P
Kent Fredric,

27
"In perl, generano gli stessi codici operativi" ... sì, ma quale è più veloce? :-)
Tin Man,

6
Adoro il fatto che gcc abbia sostituito put () con printf (), dato che esiste solo un argomento e quindi nulla da formattare, più veloce e più sicuro! (gcc controlla anche i tag di formattazione rispetto all'elenco degli argomenti variabili.)
Lee D,

@ the Tin Man: sono equivalenti, perché il computer fa le stesse esatte operazioni: P
BlackBear

1
@snap, non è 'completamente' errato, si concentra solo sui costi di runtime. Non riesco a immaginare che tipo di situazione comporterebbe che il tempo di analisi di cicli infiniti sia il fattore decisivo per la velocità di esecuzione del programma
bdonlan

55

Usando GCC, entrambi sembrano compilare nello stesso linguaggio assembly:

L2:
        jmp     L2

20
Utilizzo di GCC con l'opzione -S (assemblare, non collegare)
Martin Cote

54

Non c'è motivo di preferire l'uno all'altro. Penso che, in while(1)particolare, while(true)sia più leggibile di for(;;), ma questa è solo la mia preferenza.


94
#define EVER ;; per (MAI) ho sempre trovato quel tipo di divertente.
Tom,

19
Che ne dici di #define ever (;;) per sempre;
Martin Cote,

16
Entrambi sembrano più leggibili in superficie, ma cerco di non definire nuove parole chiave per il mio programmatore di manutenzione (di solito io) per grattargli la testa.
Bill the Lizard,

13
@Martin che non funzionerà, perché # define's non si sostituisce all'interno di un token ed foreverè il proprio token.
Lily Chung,

2
"Cerco di non definire nuove parole chiave per la mia manutenzione" - se solo più persone adottassero quell'atteggiamento, non mi aggrapperei a tutti questi shenanigani insani e magici da gioco di prestigio ogni volta che mi giro!
tchrist,

31

Non c'è differenza secondo lo standard. 6.5.3 / 1 ha:

La dichiarazione for

for ( for-init-statement ; conditionopt ; expressionopt ) statement

è equivalente a

{
  for-init-statement
  while ( condition ) {
    statement
    expression ;
  }
}

E 6.5.3 / 2 ha:

È possibile omettere una o entrambe le condizioni e l'espressione. Una condizione mancante rende la clausola while implicita equivalente a while (true).

Quindi secondo lo standard C ++ il codice:

for (;;);

è esattamente lo stesso di:

{
  while (true) {
    ;
    ;
  }
}

4
Ciò non riguarda affatto il codice generato o le prestazioni. Lo standard definisce solo funzionalità. Naturalmente, le prestazioni saranno le stesse.
Potatoswatter,

1
Non credo sia vero che una differenza nelle prestazioni violi la regola as-if. In tal caso, ai compilatori non sarebbe consentito accelerare il codice in base alla regola as-if, ad esempio riordinando istruzioni indipendenti. I compilatori infatti fanno esattamente questo. Ma la mia copia dello standard è di sopra.
Steve Jessop,

28

Il compilatore Visual C ++ utilizzato per emettere un avviso per

while (1) 

(espressione costante) ma non per

for (;;)

Ho continuato la pratica di preferire for (;;)per quel motivo, ma non so se il compilatore lo faccia ancora in questi giorni.


l'avvertimento è probabilmente perché hai usato while (1) invece di while (true)
jrharshath,

16
vero è una costante. while (true) è un'espressione costante. Per chiunque sia interessato, l'avvertimento C4127 è documentato qui: msdn.microsoft.com/en-us/library/6t66728h(VS.80).aspx
sean e

Sì, l'avviso è ancora presente sia per 1 che per vero. Questo è il motivo per cui lo uso sempre per (;;)
Elviss Strazdins l'

26

for(;;) è un carattere in meno da digitare se si desidera andare in quella direzione per ottimizzare le cose.


21
Buono a sapersi per giocare a golf. Altrimenti un motivo scadente per scegliere una sintassi.
Adam Bellaire,

@AdamBellaire La terseness spesso aumenta la leggibilità, al di sopra di una certa soglia di abilità.
Vector Gorgoth,

20

Turbo C con questi vecchi compilatori for(;;)porta quindi a un codice più veloce while(1).

Oggi gcc, i compilatori Visual C (penso quasi tutti) ottimizzano bene e le CPU con 4,7 MHz vengono utilizzate raramente.

A quei tempi un for( i=10; i; i-- )era più veloce di for( i=1; i <=10; i++ ), poiché il confronto iè 0, si traduce in un salto condizionale CPU-Zero-Flag. E lo Zero-Flag è stato modificato con l'ultima operazione di decremento ( i-- ), non è necessaria alcuna operazione cmp aggiuntiva.

    call    __printf_chk
    decl    %ebx          %ebx=iterator i 
    jnz     .L2
    movl    -4(%ebp), %ebx
    leave

e qui for(i=1; i<=10; i++)con un ulteriore cmpl:

    call    __printf_chk
    incl    %ebx
    cmpl    $11, %ebx
    jne     .L2
    movl    -4(%ebp), %ebx
    leave

13

Per tutte le persone che sostengono che non dovresti usare cicli a tempo indeterminato e suggerire cose stupide come usare open goto (seriamente, ahi)

while (1) {
     last if( condition1 );
     code();
     more_code(); 
     last if( condition2 ); 
     even_more_code(); 
}

Non può essere rappresentato in modo efficace in nessun altro modo. Non senza creare una variabile di uscita e fare magia nera per mantenerla sincronizzata.

Se hai un debole per la sintassi più goto-esque, usa qualcosa di sano che limiti l'ambito.

flow: { 

   if ( condition ){ 
      redo flow;
   }
   if ( othercondition ){ 
       redo flow;
   }
   if ( earlyexit ){ 
       last flow;
   }
   something(); # doesn't execute when earlyexit is true 
}

In definitiva, la velocità non è così importante

Preoccuparsi di quanto siano efficaci i diversi costrutti di loop in termini di velocità è una grande perdita di tempo. Ottimizzazione prematura fino in fondo. Non riesco a pensare a nessuna situazione che abbia mai visto in cui il codice di profilatura ha trovato strozzature nella mia scelta del costrutto loop.

Generalmente è il come del ciclo e il cosa del ciclo.

Dovresti "ottimizzare" per leggibilità e sintonia e scrivere tutto ciò che è meglio per spiegare il problema al prossimo povero succhiatore che trova il tuo codice.

Se usi il trucco "vai a LABEL" che qualcuno ha menzionato, e io devo usare il tuo codice, preparati a dormire con un occhio aperto, specialmente se lo fai più di una volta, perché quel genere di cose crea un codice orribile di spaghetti.

Solo perché puoi creare il codice spaghetti non significa che dovresti


9

Da Stroustrup, TC ++ PL (3a edizione), §6.1.1:

La curiosa notazione for (;;)è il modo standard per specificare un ciclo infinito; potresti pronunciarlo "per sempre". [...] while (true)è un'alternativa.

Io preferisco for (;;).


9

Se il compilatore non esegue alcuna ottimizzazione, for(;;)sarebbe sempre più veloce di while(true). Questo perché while-statement valuta la condizione ogni volta, ma for-statement è un salto incondizionato. Ma se il compilatore ottimizza il flusso di controllo, potrebbe generare alcuni codici operativi. Puoi leggere il codice di smontaggio molto facilmente.

PS potresti scrivere un ciclo infinito come questo:

#define EVER ;;
  //...
  for (EVER) {
    //...
  }

Al giorno d'oggi non dovrebbe MAI essere sostituito con SVE (parlare da adolescente)! Scherzi a parte, semplicemente uso semplicemente per (;;) {}. Ho letto online molto tempo fa sulle differenze tra i due (quando ero più giovane e in realtà non sapevo che fossero uguali) e mi sono semplicemente bloccato con quello che ho letto.
Bja,

8

Ne ho sentito parlare una volta.

Veniva da un programmatore di assemblaggio AMD. Ha dichiarato che i programmatori C (le persone) non si rendono conto che il loro codice ha inefficienze. Oggi ha detto che i compilatori gcc sono molto bravi e mettono fuori gioco persone come lui. Ha detto per esempio, e mi ha parlato del while 1vs for(;;). Lo uso ora per abitudine ma gcc e soprattutto gli interpreti faranno la stessa operazione (un salto del processore) per entrambi i giorni, poiché sono ottimizzati.


5

In una build ottimizzata di un linguaggio compilato, non dovrebbero esserci differenze apprezzabili tra i due. Nessuno dei due dovrebbe finire per eseguire alcun confronto in fase di esecuzione, eseguiranno semplicemente il codice del ciclo fino a quando non si esce manualmente dal ciclo (ad esempio con a break).


3

Sono sorpreso che nessuno sia stato testato correttamente for (;;)rispetto while (1)al perl!

Poiché perl è un linguaggio interpretato, il tempo necessario per eseguire uno script perl non consiste solo nella fase di esecuzione (che in questo caso è la stessa) ma anche nella fase di interpretazione prima dell'esecuzione. Entrambe queste fasi devono essere prese in considerazione quando si effettua un confronto di velocità.

Fortunatamente perl ha un comodo modulo Benchmark che possiamo usare per implementare un benchmark come il seguente:

#!/usr/bin/perl -w

use Benchmark qw( cmpthese );

sub t_for   { eval 'die; for (;;) { }'; }
sub t_for2  { eval 'die; for (;;)  { }'; }
sub t_while { eval 'die; while (1) { }'; }

cmpthese(-60, { for => \&t_for, for2 => \&t_for2, while => \&t_while });

Nota che sto testando due diverse versioni dell'infinito per il ciclo: una più corta del ciclo while e un'altra che ha uno spazio extra per renderlo della stessa lunghezza del ciclo while.

Su Ubuntu 11.04 x86_64 con perl 5.10.1 ottengo i seguenti risultati:

          Vota per 2 mentre
per 100588 / s - -0% -2%
for2 100937 / s 0% - -1%
mentre 102147 / s 2% 1% -

Il ciclo while è chiaramente il vincitore su questa piattaforma.

Su FreeBSD 8.2 x86_64 con perl 5.14.1:

         Vota per 2 mentre
per 53453 / s - -0% -2%
for2 53552 / s 0% - -2%
mentre 54564 / s 2% 2% -

Mentre il loop è il vincitore anche qui.

Su FreeBSD 8.2 i386 con perl 5.14.1:

         Vota mentre per for2
mentre 24311 / s - -1% -1%
per 24481 / s 1% - -1%
for2 24637 / s 1% 1% -

Sorprendentemente il ciclo for con uno spazio extra è la scelta più veloce qui!

La mia conclusione è che il ciclo while dovrebbe essere usato sulla piattaforma x86_64 se il programmatore sta ottimizzando per la velocità. Ovviamente dovrebbe essere usato un ciclo for durante l'ottimizzazione dello spazio. Purtroppo i miei risultati non sono conclusivi per quanto riguarda le altre piattaforme.


9
La conclusione è palesemente sbagliata. Benchmarkha i suoi limiti e non può essere usato per distinguere velocemente da lento se i risultati sono entro il 7% l'uno dall'altro. Inoltre, non hai testato la differenza tra i loop fore whileperché ogni sub lo farà dieprima di raggiungere i loop stessi. E da quando la quantità di spazio bianco è stata importante per l'interprete Perl? Siamo spiacenti, ma l'analisi è estremamente imperfetta.
Zaid,

2
@Zaid, grazie per i tuoi commenti! Ti dispiacerebbe pubblicare la tua risposta in modo che tutti possano imparare da quello? :) dieC'è nel mio codice perché la mia intenzione è quella di testare solo la differenza di tempo di compilazione . Come altri hanno già sottolineato, il codice byte risultante è identico, quindi non ha senso testarlo. Sorprendentemente la quantità di spazio bianco sembra fare una piccola differenza in questo caso nei miei ambienti di test. Potrebbe avere qualcosa a che fare con il modo in cui i personaggi finiscono per allinearsi nella memoria o qualcosa di simile ...
scatto il

4
Non ho bisogno di inviare una risposta perché ciò che direi è già stato menzionato da bdonlan. E anche se stai confrontando i tempi di compilazione, i numeri che Benchmarksono inconcludenti. Non fidarti di questa differenza dell'1%!
Zaid,

Solo 60 iterazioni? Esegui test per circa 5 minuti in modo da ottenere tempi relativi più precisi.
Mooing Duck,

-60esegue il test per 60 secondi.
scatto il

2

In teoria, un compilatore completamente ingenuo potrebbe memorizzare il letterale "1" nel binario (spreco di spazio) e verificare se 1 == 0 ogni iterazione (spreco di tempo e più spazio).

In realtà, tuttavia, anche con ottimizzazioni "no", i compilatori ridurranno comunque entrambi allo stesso modo. Possono anche emettere avvisi perché potrebbero indicare un errore logico. Ad esempio, l'argomento di whilepotrebbe essere definito altrove e non ti rendi conto che è costante.


2

Sono sorpreso che nessuno abbia offerto la forma più diretta, corrispondente all'assemblea desiderata:

forever:
     do stuff;
     goto forever;

Dose che non finisce con lo stesso codice macchina di mentre 1 o per (;;) diciamo c?
Copas,

1
Un altro difetto di questo approccio: viola l'incapsulamento non racchiudendo il loop in un blocco - quindi tutte le variabili dichiarate nel loop sono disponibili al di fuori del loop. (Certo che potresti { forever: do stuff; goto forever; })
Roy Tinker,

2

while(1)è un linguaggio per il for(;;)quale è riconosciuto dalla maggior parte dei compilatori.

Sono stato contento di vedere che anche il perl lo riconosce until(0).


In quale situazione sarebbe utile fino a quando (0)?
Copas,

3
fino a quando () è l'opposto di while () proprio come a meno che () sia l'opposto di if (). Come suggerito altrove in questo thread, si potrebbe scrivere: do {qualcosa ...} while (! Condizione) Un'alternativa potrebbe essere fino a (condizione) {qualcosa}
JMD

2

Riassumendo il dibattito for (;;)vs while (1)è ovvio che il primo era più veloce ai tempi dei vecchi compilatori non ottimizzanti, ecco perché si tende a vederlo in vecchie basi di codice come il commento del codice sorgente Lions Unix, tuttavia nell'era dell'ottimizzazione tosta i compilatori di questi guadagni sono ottimizzati in abbinamento accoppiando il fatto che quest'ultimo è più facile da capire rispetto al primo, credo che sarebbe più preferibile.


2

Mi sono appena imbattuto in questa discussione (anche se con qualche anno di ritardo).

Penso di aver trovato il vero motivo per cui "for (;;)" è meglio di "while (1)".

secondo lo "standard di codifica barr 2018"

Kernighan & Ritchie long ago recommended for (;;) , which has the additional benefit
of insuring against the visually-confusing defect of a while (l); referencing a variable l’.

fondamentalmente, questo non è un problema di velocità ma un problema di leggibilità. A seconda del tipo di carattere / stampa del codice, il numero uno (1) tra un po 'potrebbe apparire come una lettera minuscola l.

cioè 1 vs l. (in alcuni caratteri questi sembrano identici).

Quindi while (1) può sembrare un po 'mentre il ciclo dipende dalla variabile lettera L.

mentre (true) può anche funzionare ma in alcuni casi C e C incorporati true / false non sono ancora definiti a meno che non sia incluso stdbool.h.


2
Direi che il problema nel tuo codice sarebbe che hai una variabile chiamata l, non quella 1e che lpuò sembrare simile.
mjuopperi,

D'accordo, so che lo standard di codifica Barr dice anche altrove che le variabili devono contenere almeno 3 caratteri anche per i loop. cioè nessun i ++ ecc. in un ciclo for. Tendo a pensare che potrebbe essere un po 'troppo però. Durante la digitazione noto anche che non è solo la lettera L che assomiglia a 1. La lettera i che viene comunemente utilizzata come variabile può anche causare problemi.
Nick Law,

-3

Penso che entrambi siano gli stessi in termini di prestazioni. Ma preferirei while (1) per leggibilità, ma mi chiedo perché hai bisogno di un ciclo infinito.


-14

Loro sono la stessa cosa. Ci sono domande molto più importanti su cui riflettere.


Il mio punto implicito, ma non esplicitamente fatto sopra, è che un compilatore decente genererebbe lo stesso codice esatto per entrambi i moduli di loop. Il punto più grande è che il costrutto looping è una parte minore del tempo di esecuzione di qualsiasi algoritmo e devi prima assicurarti di aver ottimizzato l'algoritmo e tutto il resto ad esso correlato. L'ottimizzazione del costrutto del loop dovrebbe essere assolutamente in fondo all'elenco delle priorità.


22
Nessun link o spiegazione. Inutile, soggettivo e un po 'condiscendente.
cdmckay,

1
beh nessuna prova ma ha ragione. Entrambi chiamano Opcode per saltare quando falso. (che farebbe lo stesso di goto ma a nessuno piacciono i goto)
Matthew Whited,

3
Non sapevo che dovevano essere poste solo domande importanti, il mio errore era la mia prima domanda.
Copas,

3
Sì, lo ammetto, è condiscendente. Ma seriamente, anche senza alcuna prova, è ovvio che si troveranno nello stesso campo di baseball in modo rapido; se la domanda fosse sullo stile, ci sarebbe qualcosa di cui discutere. Stavo cercando di sottolineare che nella lista delle cose di cui preoccuparsi, questo dovrebbe davvero essere in fondo alla lista.
Mark Ransom,

8
Non stavo cercando di fare il cretino. Stavo cercando di fare un punto. Quando l'ho pubblicato stavo cercando una specie di umorismo oscuro, ed è ovvio che ho fallito; per questo mi scuso.
Mark Ransom,
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.