hacer - if delphi
¿Por qué es CharInSet más rápido que la declaración de caso? (5)
Estoy perplejo. En CodeRage hoy, Marco Cantu dijo que CharInSet era lento y que debería probar una declaración de caso en su lugar. Lo hice en mi analizador sintáctico y luego verifiqué con AQTime cuál era la aceleración. Encontré que el enunciado de Case es mucho más lento.
4,894,539 ejecuciones de:
mientras no CharInSet (P ^, ['''', # 10, # 13, # 0]) do inc (P);
fue cronometrado a 0.25 segundos.
Pero el mismo número de ejecuciones de:
mientras que True lo hace
caso P ^ de
'''', # 10, # 13, # 0: romper;
else inc (P);
fin;
toma .16 segundos para "while True", .80 segundos para el primer caso y .13 segundos para el caso else, totalizando 1.09 segundos o más de 4 veces más.
El código ensamblador para la instrucción CharInSet es:
agregar edi, $ 02
mov edx, $ 0064b290
movzx eax, [edi]
llamar CharInSet
prueba a1, a1
jz $ 00649f18 (a la declaración de agregar)
mientras que la lógica del caso es simplemente esto:
movzx eax, [edi]
sub ax, $ 01
jb $ 00649ef0
sub ax, $ 09
jz $ 00649ef0
sub ax, $ 03
jz $ 00649ef0
agregar edi, $ 02
jmp $ 00649ed6 (a la declaración movzx)
La lógica de casos me parece que está usando un ensamblador muy eficiente, mientras que la instrucción CharInSet en realidad tiene que hacer una llamada a la función CharInSet, que está en SysUtils y también es simple, siendo:
función CharInSet (C: AnsiChar; const CharSet: TSysCharSet): Boolean;
empezar
Resultado: = C en CharSet;
fin;
Creo que la única razón por la que se hace esto es porque P ^ in ['''', # 10, # 13, # 0] ya no está permitido en Delphi 2009, por lo que la llamada realiza la conversión de tipos para permitirlo.
No obstante, estoy muy sorprendido por esto y todavía no confío en mi resultado.
¿AQTime mide algo incorrecto? ¿Me falta algo en esta comparación, o es CharInSet realmente una función eficiente que vale la pena usar?
Conclusión:
Creo que lo tienes, Barry. Gracias por tomarse el tiempo y hacer el ejemplo detallado. Probé tu código en mi máquina y obtuve .171, .066 y .052 segundos (supongo que mi escritorio es un poco más rápido que tu computadora portátil).
Al probar ese código en AQTime, da: 0.79, 1.57 y 1.46 segundos para las tres pruebas. Ahí puedes ver la gran sobrecarga de la instrumentación. Pero lo que realmente me sorprende es que esta sobrecarga cambia el aparente "mejor" resultado para ser la función CharInSet, que en realidad es la peor.
Entonces Marcu está en lo correcto y CharInSet es más lento. Pero de manera inadvertida (o tal vez a propósito) me has dado una mejor manera al sacar lo que CharInSet está haciendo con el AnsiChar (P ^) en el método Set. Aparte de la ventaja de velocidad menor sobre el método de caso, también es menos código y más comprensible que usar los casos.
También me avisaste sobre la posibilidad de una optimización incorrecta usando AQTime (y otros perfiladores de instrumentos). Saber esto me ayudará a tomar una decisión sobre Profiler y herramientas de análisis de memoria para Delphi y también es otra respuesta a mi pregunta ¿Cómo lo hace AQTime? . Por supuesto, AQTime no cambia el código cuando se usa como instrumento, por lo que debe usar alguna otra magia para hacerlo.
Entonces, la respuesta es que AQTime está mostrando resultados que conducen a una conclusión incorrecta.
Seguimiento: dejé esta pregunta con la "acusación" de que los resultados de AQTime pueden ser engañosos. Pero, para ser justos, debería indicarle que lea esta pregunta: ¿Hay una rutina rápida de GetToken para Delphi? que comenzó pensando que AQTime dio resultados engañosos, y concluye que no.
AQTime es un perfilador de instrumentación. Los perfiladores de instrumentos a menudo no son adecuados para medir el tiempo de código, particularmente en microbenchmarks como el suyo, porque el costo de la instrumentación a menudo supera el costo de lo que se mide. Los perfiladores de instrumentos, por otro lado, sobresalen en la memoria de perfiles y en el uso de otros recursos.
Los perfiladores de muestreo, que periódicamente verifican la ubicación de la CPU, suelen ser mejores para medir el tiempo del código.
En cualquier caso, aquí hay otra microdenominación que, de hecho, muestra que una declaración de case
es más rápida que CharInSet
. Sin embargo, tenga en cuenta que la comprobación establecida todavía se puede utilizar con un tipocast para eliminar la advertencia de truncamiento (en realidad, esta es la única razón por la que existe CharInSet):
{$apptype console}
uses Windows, SysUtils;
const
SampleString = ''foo bar baz blah de;blah de blah.'';
procedure P1;
var
cp: PChar;
begin
cp := PChar(SampleString);
while not CharInSet(cp^, [#0, '';'', ''.'']) do
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 not (AnsiChar(cp^) in [#0, '';'', ''.'']) do
Inc(cp);
end;
procedure Time(const Title: string; Proc: TProc);
var
i: Integer;
start, finish, freq: Int64;
begin
QueryPerformanceCounter(start);
for i := 1 to 1000000 do
Proc;
QueryPerformanceCounter(finish);
QueryPerformanceFrequency(freq);
Writeln(Format(''%20s: %.3f seconds'', [Title, (finish - start) / freq]));
end;
begin
Time(''CharInSet'', P1);
Time(''case stmt'', P2);
Time(''set test'', P3);
end.
Su salida en mi computadora portátil aquí es:
CharInSet: 0.261 seconds
case stmt: 0.077 seconds
set test: 0.060 seconds
Barry, me gustaría señalar que su punto de referencia no refleja el rendimiento real de los diversos métodos, porque la estructura de las implementaciones difiere. En su lugar, todos los métodos deben usar un constructo "while True do", para reflejar mejor el impacto de las diferentes formas de hacer una verificación de char-in-set.
Aquí un reemplazo para los métodos de prueba (P2 no se modifica, P1 y P3 ahora usan el constructo "while True do"):
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;
Mi estación de trabajo da:
CharInSet: 0.099 seconds
case stmt: 0.043 seconds
set test: 0.043 seconds
Que mejor coincide con los resultados esperados. Para mí, parece que usar el constructo ''caso en'' realmente no ayuda. Lo siento Marco!
Como sé, llamar lleva la misma cantidad de operaciones del procesador que el salto si ambos usan punteros cortos. Con punteros largos pueden ser diferentes. La llamada en el ensamblador no usa la pila de forma predeterminada. Si hay suficientes registros gratuitos, regístrese. Así que las operaciones de pila también toman tiempo cero. Es solo registra lo que es muy rápido.
En contraste, la variante de caso usa operaciones de suma y resta que son bastante lentas y probablemente agregan la mayor parte del tiempo extra.
Un generador de perfiles de muestreo gratuito para Delphi se puede encontrar allí:
https://forums.codegear.com/thread.jspa?messageID=18506
Además de la cuestión de la medición incorrecta del tiempo de los perfiladores de instrumentación, se debe tener en cuenta que lo que es más rápido también dependerá de cuán predecibles sean las ramas del "caso". Si las pruebas en el "caso" tienen todas una probabilidad similar de ser encontradas, el rendimiento del "caso" puede terminar más bajo que el de CharInSet.
El código en la función "CharInSet" es más rápido que "caso", el tiempo se usa en "llamar", usar mientras no (cp ^ in [..]) luego
verás que este es el ayuno.