2016-03-04 34 views
9

Riscrivendo un semplice programma da C# a Go, ho trovato l'eseguibile risultante da 3 a 4 volte più lento. Esattamente la versione Go utilizza da 3 a 4 volte più CPU. È sorprendente perché il codice fa molti I/O e non dovrebbe consumare una quantità significativa di CPU.Perché vai usare cgo su Windows per un semplice File.Write?

Ho realizzato una versione molto semplice facendo solo scritture sequenziali e realizzato benchmark. Ho eseguito gli stessi benchmark su Windows 10 e Linux (Debian Jessie). Il tempo non può essere comparato (non gli stessi sistemi, dischi, ...) ma il risultato è interessante.

sto usando la stessa versione Go su entrambe le piattaforme: 1.6

In Windows os.File.Write uso CGO (vedi runtime.cgocall di seguito), non su Linux. Perché ?

Ecco il programma disk.go:

package main 

    import (
     "crypto/rand" 
     "fmt" 
     "os" 
     "time" 
    ) 

    const (
     // size of the test file 
     fullSize = 268435456 
     // size of read/write per call 
     partSize = 128 
     // path of temporary test file 
     filePath = "./bigfile.tmp" 
    ) 

    func main() { 
     buffer := make([]byte, partSize) 

     seqWrite := func() error { 
      return sequentialWrite(filePath, fullSize, buffer) 
     } 

     err := fillBuffer(buffer) 
     panicIfError(err) 
     duration, err := durationOf(seqWrite) 
     panicIfError(err) 
     fmt.Printf("Duration : %v\n", duration) 
    } 

    // It's just a test ;) 
    func panicIfError(err error) { 
     if err != nil { 
      panic(err) 
     } 
    } 

    func durationOf(f func() error) (time.Duration, error) { 
     startTime := time.Now() 
     err := f() 
     return time.Since(startTime), err 
    } 

    func fillBuffer(buffer []byte) error { 
     _, err := rand.Read(buffer) 
     return err 
    } 

    func sequentialWrite(filePath string, fullSize int, buffer []byte) error { 
     desc, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) 
     if err != nil { 
      return err 
     } 
     defer func() { 
      desc.Close() 
      err := os.Remove(filePath) 
      panicIfError(err) 
     }() 

     var totalWrote int 
     for totalWrote < fullSize { 
      wrote, err := desc.Write(buffer) 
      totalWrote += wrote 
      if err != nil { 
       return err 
      } 
     } 

     return nil 
    } 

Il test benchmark (disk_test.go):

package main 

    import (
     "testing" 
    ) 

    // go test -bench SequentialWrite -cpuprofile=cpu.out 
    // Windows : go tool pprof -text -nodecount=10 ./disk.test.exe cpu.out 
    // Linux : go tool pprof -text -nodecount=10 ./disk.test cpu.out 
    func BenchmarkSequentialWrite(t *testing.B) { 
     buffer := make([]byte, partSize) 
     err := sequentialWrite(filePath, fullSize, buffer) 
     panicIfError(err) 
    } 

Il risultato di Windows (con CGO):

11.68s of 11.95s total (97.74%) 
    Dropped 18 nodes (cum <= 0.06s) 
    Showing top 10 nodes out of 26 (cum >= 0.09s) 
      flat flat% sum%  cum cum% 
     11.08s 92.72% 92.72%  11.20s 93.72% runtime.cgocall 
     0.11s 0.92% 93.64%  0.11s 0.92% runtime.deferreturn 
     0.09s 0.75% 94.39%  11.45s 95.82% os.(*File).write 
     0.08s 0.67% 95.06%  0.16s 1.34% runtime.deferproc.func1 
     0.07s 0.59% 95.65%  0.07s 0.59% runtime.newdefer 
     0.06s 0.5% 96.15%  0.28s 2.34% runtime.systemstack 
     0.06s 0.5% 96.65%  11.25s 94.14% syscall.Write 
     0.05s 0.42% 97.07%  0.07s 0.59% runtime.deferproc 
     0.04s 0.33% 97.41%  11.49s 96.15% os.(*File).Write 
     0.04s 0.33% 97.74%  0.09s 0.75% syscall.(*LazyProc).Find 

Il risultato di Linux (senza cgo):

5.04s of 5.10s total (98.82%) 
    Dropped 5 nodes (cum <= 0.03s) 
    Showing top 10 nodes out of 19 (cum >= 0.06s) 
      flat flat% sum%  cum cum% 
     4.62s 90.59% 90.59%  4.87s 95.49% syscall.Syscall 
     0.09s 1.76% 92.35%  0.09s 1.76% runtime/internal/atomic.Cas 
     0.08s 1.57% 93.92%  0.19s 3.73% runtime.exitsyscall 
     0.06s 1.18% 95.10%  4.98s 97.65% os.(*File).write 
     0.04s 0.78% 95.88%  5.10s 100% _/home/sam/Provisoire/go-disk.sequentialWrite 
     0.04s 0.78% 96.67%  5.05s 99.02% os.(*File).Write 
     0.04s 0.78% 97.45%  0.04s 0.78% runtime.memclr 
     0.03s 0.59% 98.04%  0.08s 1.57% runtime.exitsyscallfast 
     0.02s 0.39% 98.43%  0.03s 0.59% os.epipecheck 
     0.02s 0.39% 98.82%  0.06s 1.18% runtime.casgstatus 
+1

Ricordo di aver letto da qualche parte che i sistemi operativi Windows non hanno un'interfaccia di syscall come fanno i sistemi unix ed espongono invece un'API C. Non sono sicuro di quanto sia vero. –

+1

Potrei sbagliarmi, ma guardando https://github.com/golang/go/blob/master/src/syscall/zsyscall_windows.go, sembra come se tutti i sysc fossero passati attraverso cgo, ma non ho molta familiarità con finestre. questa domanda si adatterebbe meglio sulla ML golanella. – OneOfOne

+1

Infatti Windows * do * ha syscalls. Il problema è che, contrariamente ad alcuni altri kernel del sistema operativo (incluso Linux) che hanno tabelle di numeri di syscall stabili, che si estendono solo aggiungendo nuove syscalls, Windows non ha mai pubblicato questi numeri, e * fanno * differiscono tra diverse versioni dei kernel di questa famiglia di sistemi operativi. E potrebbero persino essere legittimamente diversi tra, diciamo, diversi service pack. Poiché l'unico modo documentato per accedere a queste syscalls è tramite DLL, è ciò che si suppone che Go stia facendo. – kostix

risposta

5

Go non esegue l'I/O file, delega l'attività al sistema operativo. Vedere i pacchetti syscall dipendenti dal sistema operativo Go.

Linux e Windows sono diversi sistemi operativi con SO diversi OS. Ad esempio, Linux utilizza syscalls tramite syscall.Syscall e Windows utilizza le dll di Windows. Su Windows, la chiamata dll è una chiamata C. Non utilizza cgo. Passa attraverso lo stesso controllo dinamico del puntatore C utilizzato da cgo, runtime.cgocall. Non c'è l'alias runtime.wincall.

In sintesi, diversi sistemi operativi hanno meccanismi di chiamata del sistema operativo diversi.

Command cgo

Passing pointers

Go è un linguaggio di garbage collection, e il garbage collector ha bisogno di conoscere la posizione di ogni puntatore a Go di memoria. A causa di questo, ci sono restrizioni sul passaggio di puntatori tra Go e C.

In questa sezione il puntatore Go termine significa un puntatore alla memoria allocata da Go (ad esempio utilizzando l'operatore & o chiamando il nuovo predefinito funzione) e il puntatore C termine indica un puntatore alla memoria allocata da C (ad esempio tramite una chiamata a C.malloc). Se un puntatore è un puntatore Go o un puntatore C è una proprietà dinamica determinata dal modo in cui è stata allocata la memoria; non ha nulla a che fare con il il tipo di puntatore.

Il codice Go può passare un puntatore Go a C fornito la memoria Go a cui i punti non contengono puntatori Go.Il codice C deve conservare questa proprietà : non deve memorizzare temporaneamente alcun puntatore Go nella memoria Go, anche . Quando si passa un puntatore a un campo in una struttura, la memoria Go in questione è la memoria occupata dal campo, non l'intera struttura . Quando si passa un puntatore a un elemento in una matrice o una sezione, la memoria di spostamento in questione è l'intero array o l'intero array di supporto della sezione.

Il codice C potrebbe non conservare una copia di un puntatore Go dopo la chiamata.

Una funzione Go chiamata dal codice C potrebbe non restituire un puntatore Go. Una funzione Go chiamata dal codice C può richiedere i puntatori C come argomenti e potrebbe importare dati di puntatore non puntatore o C attraverso quei puntatori, ma potrebbe non memorizzare un puntatore Go in memoria puntato da un puntatore C. Una funzione Go chiamata dal codice C può richiedere un puntatore Go come argomento, ma è necessario che conservi la proprietà a cui punta la memoria Go a cui punta .

Il codice Go non può memorizzare un puntatore Go in memoria C. Il codice C può memorizzare i puntatori Go nella memoria C, in base alla regola precedente: è necessario interrompere la memorizzazione del puntatore Go quando la funzione C restituisce .

Queste regole vengono verificate dinamicamente in fase di esecuzione. Il controllo è controllato dall'impostazione cgocheck della variabile dell'ambiente GODEBUG. L'impostazione predefinita è GODEBUG = cgocheck = 1, che implementa i controlli dinamici ragionevolmente economici . Questi controlli possono essere disabilitati interamente utilizzando GODEBUG = cgocheck = 0. Il controllo completo della gestione dei puntatori, al costo di in fase di esecuzione, è disponibile tramite GODEBUG = cgocheck = 2.

È possibile sconfiggere questa imposizione utilizzando il pacchetto non sicuro, e, naturalmente, non c'è nulla che impedisca al codice C di eseguire operazioni simili a . Tuttavia, è probabile che i programmi che infrangono queste regole falliscano in modi imprevedibili e imprevedibili.

"Queste regole vengono verificate dinamicamente in fase di esecuzione."


Benchmark:

Per parafrasare, ci sono bugie, maledette bugie, e parametri di riferimento.

Per confronti validi tra sistemi operativi è necessario eseguire su hardware identico. Ad esempio, la differenza tra CPU, memoria e ruggine o I/O del disco di silicio. Ho dual-boot Linux e Windows sullo stesso computer.

Eseguire benchmark almeno tre volte back-to-back. I sistemi operativi cercano di essere intelligenti. Ad esempio, caching I/O. Le lingue che utilizzano macchine virtuali necessitano di un tempo di riscaldamento. E così via.

Sapere cosa si sta misurando. Se si esegue l'I/O sequenziale, si trascorre quasi tutto il tempo nel sistema operativo. Hai disattivato la protezione da malware? E così via.

E così via.

Ecco alcuni risultati per disk.go dalla stessa macchina utilizzando dual-boot Windows e Linux.

di Windows:

>go build disk.go 
>/TimeMem disk 
Duration : 18.3300322s 
Elapsed time : 18.38 
Kernel time : 13.71 (74.6%) 
User time  : 4.62 (25.1%) 

Linux:

$ go build disk.go 
$ time ./disk 
Duration : 18.54350723s 
real 0m18.547s 
user 0m2.336s 
sys  0m16.236s 

Effettivamente, sono la stessa cosa, 18 secondi disk.go durata. Solo qualche variazione tra i sistemi operativi in ​​merito a ciò che viene conteggiato il tempo utente e ciò che viene contato come tempo del kernel o del sistema. Il tempo trascorso o in tempo reale è lo stesso.

Nei test, il tempo del kernel o del sistema era 93.72% runtime.cgocall versus 95.49% syscall.Syscall.

+0

Grazie. Quindi 'runtime.cgocall' non significa che usi cgo. Ma fa un lavoro simile. – samonzeweb

+0

@samonzeweb: il tempo 'runtime.cgocall' misura il tempo trascorso fuori da Go quando chiama tramite' cgo' e, su Windows, quando chiama 'dll's di Windows. Nel tuo programma 'disk.go' tutte le chiamate all'esterno di Go sono su' dll's di Windows. – peterSO

+0

@samonzeweb: vedere la mia risposta rivista per alcuni commenti su benchmark e alcuni risultati di benchmark. – peterSO