Quali sono i pericoli quando si crea un thread con una dimensione dello stack di 50 volte quella predefinita?


228

Attualmente sto lavorando a un programma molto critico per le prestazioni e un percorso che ho deciso di esplorare che potrebbe aiutare a ridurre il consumo di risorse è stato aumentare le dimensioni dello stack dei miei thread di lavoro in modo da poter spostare la maggior parte dei dati a float[]cui accederò la pila (usando stackalloc).

Ho letto che la dimensione predefinita dello stack per un thread è 1 MB, quindi per spostare tutti i miei float[]s dovrei espandere lo stack di circa 50 volte (a 50 MB ~).

Capisco che questo è generalmente considerato "non sicuro" e non è raccomandato, ma dopo aver confrontato il mio codice attuale con questo metodo, ho scoperto un aumento del 530% della velocità di elaborazione! Quindi non posso semplicemente passare con questa opzione senza ulteriori indagini, il che mi porta alla mia domanda; quali sono i pericoli associati all'aumento della pila di dimensioni così grandi (cosa potrebbe andare storto) e quali precauzioni dovrei prendere per minimizzare tali pericoli?

Il mio codice di prova,

public static unsafe void TestMethod1()
{
    float* samples = stackalloc float[12500000];

    for (var ii = 0; ii < 12500000; ii++)
    {
        samples[ii] = 32768;
    }
}

public static void TestMethod2()
{
    var samples = new float[12500000];

    for (var i = 0; i < 12500000; i++)
    {
        samples[i] = 32768;
    }
}

98
+1. Sul serio. Ti chiedi cosa Sembri una domanda idiota fuori dalla norma e poi fai un ottimo caso che nel tuo particolare scenario è una cosa sensata da considerare perché hai fatto i compiti e misurato il risultato. Questo è MOLTO buono - mi manca con molte domande. Molto bello - bene consideri qualcosa del genere, purtroppo molti programmatori C # non sono a conoscenza di queste opportunità di ottimizzazione. Sì, spesso non è necessario, ma a volte è fondamentale e fa la differenza.
TomTom,

5
Sono interessato a vedere i due codici che hanno una differenza del 530% nella velocità di elaborazione, solo a causa dello spostamento dell'array nello stack. Questo non sembra giusto.
Dialecticus,

13
Prima di saltare su questa strada: hai provato a usare Marshal.AllocHGlobal(non dimenticare di farlo FreeHGlobal) per allocare i dati al di fuori della memoria gestita? Quindi lancia il puntatore su a float*e dovresti essere ordinato.
Marc Gravell

2
È giusto fare molte allocazioni. Stackalloc evita tutti i problemi di GC che possono anche creare / creare una località molto forte a livello di processore. Questa è una delle cose che sembrano micro ottimizzazioni - a meno che tu non scriva un programma matematico ad alte prestazioni e stia avendo esattamente questo comportamento e fa la differenza;)
TomTom

6
Il mio sospetto: uno di questi metodi innesca il controllo dei limiti su ogni iterazione di loop mentre l'altro non lo fa, oppure è ottimizzato via.
pjc50,

Risposte:


45

Confrontando il codice di prova con Sam, ho determinato che entrambi abbiamo ragione!
Tuttavia, su cose diverse:

  • L'accesso alla memoria (lettura e scrittura) è altrettanto veloce ovunque sia - stack, global o heap.
  • Allocare , tuttavia, è più veloce nello stack e più lento nell'heap.

Va in questo modo: stack< global< heap. (tempo di allocazione)
Tecnicamente, l'allocazione dello stack non è in realtà un'allocazione, il runtime si assicura solo che una parte dello stack (frame?) sia riservata all'array.

Consiglio vivamente di stare attento con questo, però.
Raccomando quanto segue:

  1. Quando è necessario creare frequentemente array che non escono mai dalla funzione (ad esempio passando il suo riferimento), l'utilizzo dello stack sarà un enorme miglioramento.
  2. Se riesci a riciclare un array, fallo ogni volta che puoi! L'heap è il posto migliore per l'archiviazione di oggetti a lungo termine. (inquinare la memoria globale non è bello; i frame dello stack possono scomparire)

( Nota : 1. si applica solo ai tipi di valore; i tipi di riferimento verranno allocati sull'heap e il vantaggio verrà ridotto a 0)

Per rispondere alla domanda stessa: non ho riscontrato alcun problema con nessun test di grandi dimensioni.
Credo che gli unici possibili problemi siano un overflow dello stack, se non stai attento con le tue chiamate di funzione e esaurisci la memoria quando crei i tuoi thread se il sistema sta per finire.

La sezione seguente è la mia risposta iniziale. È sbagliato e i test non sono corretti. È conservato solo come riferimento.


Il mio test indica che la memoria allocata in stack e la memoria globale è almeno del 15% più lenta della memoria allocata in heap (impiega il 120% del tempo) per l'utilizzo in array!

Questo è il mio codice di test e questo è un output di esempio:

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

Ho provato su Windows 8.1 Pro (con aggiornamento 1), usando un i7 4700 MQ, sotto .NET 4.5.1
Ho provato sia con x86 che x64 e i risultati sono identici.

Modifica : ho aumentato la dimensione dello stack di tutti i thread 201 MB, la dimensione del campione a 50 milioni e ridotto le iterazioni a 5.
I risultati sono gli stessi sopra :

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

Tuttavia, sembra che lo stack stia effettivamente diventando più lento .


Dovrei essere in disaccordo, secondo i risultati del mio benchmark (vedere il commento in fondo alla pagina per i risultati) mostra che lo stack è leggermente più veloce del globale e molto più veloce dell'heap; e per essere sicuramente sicuro che i miei risultati siano accurati, ho eseguito il test 20 volte e ogni metodo è stato chiamato 100 volte per iterazione del test. Stai sicuramente eseguendo correttamente il tuo benchmark?
Sam,

Sto ottenendo risultati molto incoerenti. Con piena fiducia, x64, config di rilascio, nessun debugger, sono tutti ugualmente veloci (meno dell'1% di differenza; fluttuanti) mentre il tuo è davvero molto più veloce con uno stack. Ho bisogno di testare ulteriormente! Modifica : il tuo DOVREBBE lanciare un'eccezione di overflow dello stack. Basta allocare abbastanza per l'array. O_o
Vercas,

Sì, lo so, è vicino. È necessario ripetere alcune volte i benchmark, come ho fatto io, magari provare a fare una media di oltre 5 corse.
Sam,

1
@Voo La prima manche ha impiegato tanto tempo quanto la centesima corsa di qualsiasi test per me. Dalla mia esperienza, questa cosa Java JIT non si applica affatto a .NET. L'unico "warm up" eseguito da .NET è il caricamento di classi e assembly quando vengono utilizzati per la prima volta.
Vercas,

2
@Voo Metti alla prova il mio benchmark e quello dell'essenza che ha aggiunto in un commento a questa risposta. Metti insieme i codici ed esegui alcune centinaia di test. Quindi torna e segnala le tue conclusioni. Ho fatto i miei test molto accuratamente, e so benissimo di cosa sto parlando quando dico che .NET non interpreta alcun bytecode come fa Java, lo JIT immediatamente.
Vercas,

28

Ho scoperto un aumento del 530% della velocità di elaborazione!

Questo è di gran lunga il più grande pericolo che direi. C'è qualcosa di gravemente sbagliato nel tuo benchmark, il codice che si comporta in modo imprevedibile di solito ha un brutto bug nascosto da qualche parte.

È molto, molto difficile consumare molto spazio nello stack in un programma .NET, a parte una ricorsione eccessiva. Le dimensioni del frame dello stack dei metodi gestiti sono impostate in pietra. Semplicemente la somma degli argomenti del metodo e delle variabili locali in un metodo. Meno quelli che possono essere memorizzati in un registro CPU, è possibile ignorarlo poiché ce ne sono così pochi.

Aumentare le dimensioni dello stack non compie nulla, ti riserverai solo un sacco di spazio indirizzo che non verrà mai utilizzato. Non esiste alcun meccanismo che possa spiegare un aumento del perf dal non usare la memoria ovviamente.

Questo a differenza di un programma nativo, in particolare uno scritto in C, può anche riservare spazio per array sul frame dello stack. Il vettore di attacco malware di base dietro gli overflow del buffer dello stack. Possibile anche in C #, dovresti usare ilstackalloc parola chiave. Se lo stai facendo, allora l'ovvio pericolo è dover scrivere codice non sicuro soggetto a tali attacchi, nonché corruzione casuale dello stack. Bug molto difficili da diagnosticare. Esiste una contromisura contro questo nei jitter successivi, penso a partire da .NET 4.0, dove il jitter genera codice per inserire un "cookie" nel frame dello stack e controlla se è ancora intatto quando il metodo ritorna. Arresto anomalo istantaneo del desktop senza alcun modo di intercettare o segnalare l'incidente se ciò accade. Questo è ... pericoloso per lo stato mentale dell'utente.

Il thread principale del tuo programma, quello avviato dal sistema operativo, avrà uno stack di 1 MB per impostazione predefinita, 4 MB quando compili il tuo programma per x64. Un aumento che richiede l'esecuzione di Editbin.exe con l'opzione / STACK in un evento post build. In genere è possibile richiedere fino a 500 MB prima che il programma abbia difficoltà ad avviarsi durante l'esecuzione in modalità a 32 bit. Anche i thread possono, molto più facilmente, la zona pericolosa si aggira intorno ai 90 MB per un programma a 32 bit. Attivato quando il programma è in esecuzione da molto tempo e lo spazio degli indirizzi è stato frammentato dalle allocazioni precedenti. L'utilizzo totale dello spazio degli indirizzi deve essere già elevato, nel corso di un concerto, per ottenere questa modalità di errore.

Controlla tre volte il tuo codice, c'è qualcosa di molto sbagliato. Non è possibile ottenere uno speedup x5 con uno stack più grande se non si scrive esplicitamente il codice per sfruttarlo. Che richiede sempre un codice non sicuro. L'uso dei puntatori in C # ha sempre un talento per la creazione di codice più veloce, non è soggetto ai controlli dei limiti dell'array.


21
Lo speedup 5x riportato è stato il passaggio da float[]a float*. Il grande stack è stato semplicemente come è stato realizzato. Uno speedup x5 in alcuni scenari è del tutto ragionevole per quel cambiamento.
Marc Gravell

3
Ok, non avevo ancora lo snippet di codice quando ho iniziato a rispondere alla domanda. Ancora abbastanza vicino.
Hans Passant,

22

Avrei una prenotazione lì che semplicemente non saprei come prevederlo - permessi, GC (che deve scansionare lo stack), ecc. - Tutto potrebbe essere influenzato. Sarei molto tentato di utilizzare invece la memoria non gestita:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}

1
Domanda a margine: perché il GC dovrebbe scannerizzare lo stack? La memoria allocata da stackallocnon è soggetta alla garbage collection.
dcastro,

6
@dcastro ha bisogno di scansionare lo stack per verificare i riferimenti che esistono solo nello stack. Semplicemente non so cosa succederà quando arriverà a un così grande stackalloc- ha bisogno di saltarlo, e speri che lo faccia così senza sforzo - ma il punto che sto cercando di fare è che introduce complicazioni / preoccupazioni inutili . IMO, stackallocè ottimo come buffer di memoria, ma per uno spazio di lavoro dedicato, è più probabile che allochi un chunk-o-memory da qualche parte, piuttosto che abusare / confondere lo stack,
Marc Gravell

8

Una cosa che può andare storta è che potresti non avere il permesso per farlo. A meno che non vengano eseguiti in modalità di affidabilità completa, Framework ignorerà semplicemente la richiesta di dimensioni dello stack più grandi (vedere MSDN suThread Constructor (ParameterizedThreadStart, Int32) )

Invece di aumentare le dimensioni dello stack di sistema a numeri così grandi, suggerirei di riscrivere il codice in modo che utilizzi Iteration e un'implementazione manuale dello stack sull'heap.


1
Buona idea, invece passerò in rassegna. Oltre a ciò, il mio codice viene eseguito in modalità di affidabilità completa, quindi ci sono altre cose che dovrei cercare?
Sam,

6

Gli array ad alte prestazioni potrebbero essere accessibili allo stesso modo di un normale C # one ma potrebbe essere l'inizio del problema: considerare il codice seguente:

float[] someArray = new float[100]
someArray[200] = 10.0;

Ci si aspetta un'eccezione fuori limite e questo ha perfettamente senso perché si sta tentando di accedere all'elemento 200 ma il valore massimo consentito è 99. Se si passa alla route stackalloc, non vi sarà alcun oggetto racchiuso intorno all'array per il controllo associato e il di seguito non mostrerà alcuna eccezione:

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

Sopra stai allocando memoria sufficiente per contenere 100 float e stai impostando la dimensione della memoria (float) posizione che inizia nella posizione iniziata di questa memoria + 200 * sizeof (float) per contenere il tuo valore float 10. Non sorprende che questa memoria sia al di fuori del memoria allocata per i float e nessuno saprebbe cosa potrebbe essere memorizzato in quell'indirizzo. Se sei fortunato potresti aver usato un po 'di memoria attualmente inutilizzata ma allo stesso tempo è probabile che tu possa sovrascrivere una posizione che è stata usata per memorizzare altre variabili. Riassumendo: comportamento di runtime imprevedibile.


Realmente sbagliato. I test di runtime e compilatore sono ancora lì.
TomTom,

9
@TomTom erm, no; la risposta ha merito; la domanda parla stackalloc, nel qual caso stiamo parlando di float*etc - che non ha gli stessi controlli. Si chiama unsafeper un'ottima ragione. Personalmente sono perfettamente felice di usare unsafequando c'è una buona ragione, ma Socrate fa alcuni punti ragionevoli.
Marc Gravell

@Marc Per il codice mostrato (dopo l'esecuzione di JIT) non ci sono più controlli dei limiti perché è banale per il compilatore ragionare che tutti gli accessi sono in-bound. In generale, tuttavia, questo può sicuramente fare la differenza.
Voo,

6

I linguaggi di microbenchmarking con JIT e GC come Java o C # possono essere un po 'complicati, quindi è generalmente una buona idea usare un framework esistente - Java offre mhf o Caliper che sono eccellenti, purtroppo per quanto ne so C # non offre qualsiasi cosa si avvicini a quelli. Jon Skeet ha scritto questo qui che darò per scontato ciecamente si prende cura delle cose più importanti (Jon sa quello che sta facendo in quella zona; inoltre si no preoccupa ho fatto in realtà controllare). Ho modificato un po 'i tempi perché 30 secondi per test dopo il riscaldamento erano troppo per la mia pazienza (5 secondi dovrebbero fare).

Quindi, prima i risultati, .NET 4.5.1 in Windows 7 x64: i numeri indicano le iterazioni che potrebbero essere eseguite in 5 secondi, quindi è meglio andare più in alto.

x64 JIT:

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT (sì, è ancora un po 'triste):

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

Questo dà una velocità molto più ragionevole al massimo del 14% (e la maggior parte delle spese generali è dovuta al fatto che il GC deve funzionare, considerandolo realisticamente uno scenario peggiore). I risultati x86 sono comunque interessanti, non del tutto chiari cosa sta succedendo lì.

ed ecco il codice:

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}

Un'osservazione interessante, dovrò controllare nuovamente i miei parametri di riferimento. Anche se questo non risponde alla mia domanda, " ... quali sono i pericoli associati all'aumento dello stack di dimensioni così grandi ... ". Anche se i miei risultati non sono corretti, la domanda è ancora valida; Apprezzo comunque lo sforzo.
Sam,

1
@Sam Quando utilizzo 12500000come dimensione ottengo effettivamente un'eccezione stackoverflow . Ma principalmente si trattava di rifiutare la premessa di base secondo cui l'uso del codice allocato in stack è più rapido di diversi ordini di grandezza. Stiamo facendo praticamente la minima quantità di lavoro possibile qui altrimenti e la differenza è già solo di circa il 10-15% - in pratica sarà ancora più bassa .. questo secondo me cambia decisamente l'intera discussione.
Voo,

5

Poiché la differenza di prestazioni è troppo grande, il problema è appena correlato all'allocazione. È probabilmente causato dall'accesso all'array.

Ho smontato il corpo del loop delle funzioni:

TestMethod1:

IL_0011:  ldloc.0 
IL_0012:  ldloc.1 
IL_0013:  ldc.i4.4 
IL_0014:  mul 
IL_0015:  add 
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1 
IL_001d:  ldc.i4.1 
IL_001e:  add 
IL_001f:  stloc.1 
IL_0020:  ldloc.1 
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2:

IL_0012:  ldloc.0 
IL_0013:  ldloc.1 
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1 
IL_001b:  ldc.i4.1 
IL_001c:  add 
IL_001d:  stloc.1 
IL_001e:  ldloc.1 
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

Possiamo verificare l'utilizzo delle istruzioni e, soprattutto, l'eccezione che generano nelle specifiche ECMA :

stind.r4: Store value of type float32 into memory at address

Eccezioni che genera:

System.NullReferenceException

E

stelem.r4: Replace array element at index with the float32 value on the stack.

Eccezione che genera:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Come puoi vedere, stelemfunziona di più nel controllo dell'intervallo di array e nel controllo del tipo. Poiché il corpo del loop fa poca cosa (assegna solo valore), l'overhead del controllo domina il tempo di calcolo. Ecco perché le prestazioni differiscono del 530%.

E questo risponde anche alle tue domande: il pericolo è l'assenza della gamma di array e del controllo del tipo. Questo non è sicuro (come indicato nella dichiarazione di funzione; D).


4

EDIT: (una piccola modifica nel codice e nella misurazione produce un grande cambiamento nel risultato)

Innanzitutto ho eseguito il codice ottimizzato nel debugger (F5) ma era sbagliato. Dovrebbe essere eseguito senza il debugger (Ctrl + F5). In secondo luogo, il codice può essere completamente ottimizzato, quindi dobbiamo complicarlo in modo che l'ottimizzatore non interferisca con la nostra misurazione. Ho fatto in modo che tutti i metodi restituissero un ultimo elemento nella matrice e la matrice è popolata in modo diverso. Inoltre c'è un ulteriore zero nei PO TestMethod2che lo rende sempre dieci volte più lento.

Ho provato alcuni altri metodi, oltre ai due che hai fornito. Il metodo 3 ha lo stesso codice del metodo 2, ma la funzione è dichiarata unsafe. Il metodo 4 utilizza l'accesso del puntatore all'array creato regolarmente. Il metodo 5 utilizza l'accesso del puntatore alla memoria non gestita, come descritto da Marc Gravell. Tutti e cinque i metodi funzionano in tempi molto simili. M5 è il più veloce (e M1 è il secondo vicino). La differenza tra il più veloce e il più lento è del 5% circa, il che non mi interessa.

    public static unsafe float TestMethod3()
    {
        float[] samples = new float[5000000];

        for (var ii = 0; ii < 5000000; ii++)
        {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }

        return samples[5000000 - 1];
    }

    public static unsafe float TestMethod4()
    {
        float[] prev = new float[5000000];
        fixed (float* samples = &prev[0])
        {
            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
    }

    public static unsafe float TestMethod5()
    {
        var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float));
        try
        {
            float* samples = (float*)ptr;

            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }

Quindi M3 è uguale a M2 contrassegnato solo con "non sicuro"? Piuttosto sospetto che sarebbe più veloce ... sei sicuro?
Roman Starkov,

@romkyns Ho appena eseguito un benchmark (M2 vs M3) e sorprendentemente M3 è in realtà il 2,14% più veloce di M2.
Sam,

" La conclusione è che non è necessario usare lo stack " quando si allocano blocchi di grandi dimensioni come quello che ho dato nel mio post, sono d'accordo, ma, dopo aver appena completato alcuni altri benchmark M1 vs M2 (usando l'idea di PFM per entrambi i metodi) sicuramente non sono d'accordo, poiché M1 è ora il 135% più veloce di M2.
Sam,

1
@Sam Ma stai ancora confrontando l'accesso del puntatore all'accesso dell'array! Questo è principalmente ciò che lo rende più veloce. TestMethod4vs TestMethod1è un confronto molto migliore per stackalloc.
Roman Starkov,

@romkyns Ah sì buon punto, me ne sono dimenticato; Ho rieseguito i benchmark , adesso c'è solo una differenza dell'8% (M1 è il più veloce dei due).
Sam,
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.