Perché CharInSet è più veloce dell'istruzione Case?
-
22-07-2019 - |
Domanda
Sono perplesso. Oggi a CodeRage, Marco Cantu ha affermato che CharInSet era lento e che avrei dovuto provare invece un'istruzione Case. L'ho fatto nel mio parser e poi ho verificato con AQTime quale fosse lo speedup. Ho trovato l'istruzione Case molto più lenta.
4.894.539 esecuzioni di:
pur non essendo CharInSet (P ^, ['', # 10, # 13, # 0]) do inc (P);
è stato cronometrato a 0,25 secondi.
Ma lo stesso numero di esecuzioni di:
mentre True fa
& nbsp; & nbsp; caso P ^ di
& Nbsp; & nbsp; & nbsp; & nbsp;' ', # 10, # 13, # 0: pausa;
& nbsp; & nbsp; & nbsp; & nbsp; else inc (P);
& Nbsp; & nbsp; fine;
impiega .16 secondi per " mentre True " ;, .80 secondi per il primo caso e .13 secondi per l'altro caso, per un totale di 1,09 secondi o oltre 4 volte di più.
Il codice assembler per l'istruzione CharInSet è:
aggiungi edi, $ 02
mov edx, $ 0064b290
movzx eax, [edi]
chiama CharInSet
test a1, a1
jz $ 00649f18 (torna all'istruzione add)
considerando che la logica del caso è semplicemente questa:
movzx eax, [edi]
sotto ascia, $ 01
jb $ 00649ef0
sotto ascia, $ 09
jz $ 00649ef0
sotto ascia, $ 03
jz $ 00649ef0
aggiungi edi, $ 02
jmp $ 00649ed6 (torna all'istruzione movzx)
La logica del case mi sembra che stia usando un assemblatore molto efficiente, mentre l'istruzione CharInSet deve effettivamente effettuare una chiamata alla funzione CharInSet, che è in SysUtils ed è anche semplice, essendo:
funzione CharInSet (C: AnsiChar; const CharSet: TSysCharSet): Boolean;
iniziare
Risultato: = C in CharSet;
fine;
Penso che l'unico motivo per cui questo sia fatto sia perché P ^ in ['', # 10, # 13, # 0] non è più consentito in Delphi 2009, quindi la chiamata esegue la conversione dei tipi per consentirlo.
Tuttavia sono molto sorpreso da questo e ancora non mi fido del mio risultato.
AQTime sta misurando qualcosa di sbagliato, mi sto perdendo qualcosa in questo confronto o CharInSet è davvero una funzione efficiente che vale la pena usare?
Conclusione:
Penso che tu l'abbia capito, Barry. Grazie per aver dedicato del tempo e fatto l'esempio dettagliato. Ho testato il tuo codice sul mio computer e ho ottenuto .171, .066 e .052 secondi (suppongo che il mio desktop sia un po 'più veloce del tuo laptop).
Test quel codice in AQTime, dà: 0,79, 1,57 e 1,46 secondi per i tre test. Lì puoi vedere il grande overhead dalla strumentazione. Ma ciò che mi sorprende davvero è che questo sovraccarico cambia l'apparente "migliore" risulta essere la funzione CharInSet che in realtà è la peggiore.
Quindi Marcu è corretto e CharInSet è più lento. Ma inavvertitamente (o forse di proposito) mi hai dato un modo migliore tirando fuori cosa sta facendo CharInSet con il metodo AnsiChar (P ^) in Set. Oltre al vantaggio di velocità minore rispetto al metodo case, è anche meno codice e più comprensibile rispetto all'utilizzo dei casi.
Mi hai anche reso conto della possibilità di un'ottimizzazione errata utilizzando AQTime (e altri profilatori di strumenti). Sapere questo mi aiuterà a prendere Strumenti di analisi del profilo e della memoria per Delphi ed è anche un'altra risposta alla mia domanda Come funziona AQTime? . Ovviamente, AQTime non cambia il codice quando lo strumento, quindi deve usare qualche altra magia per farlo.
Quindi la risposta è che AQTime sta mostrando risultati che portano a conclusioni errate.
Altri suggerimenti
Barry, vorrei sottolineare che il tuo benchmark non riflette le prestazioni effettive dei vari metodi, perché la struttura delle implementazioni differisce. Invece, tutti i metodi dovrebbero usare un " mentre True do " costruire, per riflettere meglio l'impatto dei diversi modi di effettuare un controllo a caratteri.
Qui un sostituto per i metodi di test (P2 è invariato, P1 e P3 ora usano il costrutto "quot" mentre True fa "):
procedure P1;
var
cp: PChar;
begin
cp := PChar(SampleString);
while True do
if CharInSet(cp^, [#0, ';', '.']) then
Break
else
Inc(cp);
end;
procedure P2;
var
cp: PChar;
begin
cp := PChar(SampleString);
while True do
case cp^ of
'.', #0, ';':
Break;
else
Inc(cp);
end;
end;
procedure P3;
var
cp: PChar;
begin
cp := PChar(SampleString);
while True do
if AnsiChar(cp^) in [#0, ';', '.'] then
Break
else
Inc(cp);
end;
La mia workstation fornisce:
CharInSet: 0.099 seconds
case stmt: 0.043 seconds
set test: 0.043 seconds
Che corrisponde meglio ai risultati previsti. Per me, sembra che usare il costrutto "case in" non sia davvero d'aiuto. Mi dispiace Marco!
Un profiler di campionamento gratuito per Delphi è disponibile qui:
https://forums.codegear.com/thread.jspa?messageID=18506
Oltre alla questione della misurazione errata del tempo dei profilatori di strumentazione, va notato che ciò che è più veloce dipenderà anche dalla prevedibilità del "caso". i rami sono. Se i test nel "caso" hanno tutte una probabilità simile di essere incontrato, prestazioni di "caso" può essere inferiore a quello di CharInSet.
Il codice nella funzione " CharInSet " è più veloce di "case", il tempo è dedicato a "call", uso mentre no (cp ^ in [..]) quindi
vedrai che questo è il digiuno.
Come so, la chiamata richiede la stessa quantità di operazioni del processore come il salto, se utilizzano entrambi puntatori brevi. Con puntatori lunghi possono essere diversi. La chiamata nell'assemblatore non utilizza lo stack per impostazione predefinita. Se c'è abbastanza registro libero usato registrati. Quindi anche le operazioni dello stack richiedono zero tempo. Sono solo i registri ad essere molto veloci.
Nella variante del caso contrario, come vedo, usa le operazioni add e sub che sono piuttosto lente e probabilmente aggiungono la maggior parte degli extratime.