¿Cuándo optimizar la memoria frente a la velocidad de rendimiento para un método?

107

Recientemente me entrevisté en Amazon. Durante una sesión de codificación, el entrevistador preguntó por qué declaraba una variable en un método. Le expliqué mi proceso y él me retó a resolver el mismo problema con menos variables. Por ejemplo (esto no fue de la entrevista), comencé con el Método A y luego lo mejoré al Método B, al eliminarlo int s. Estaba satisfecho y dijo que esto reduciría el uso de memoria por este método.

Entiendo la lógica detrás de esto, pero mi pregunta es:

¿Cuándo es apropiado usar el Método A versus el Método B y viceversa?

Puede ver que el Método A tendrá un mayor uso de memoria, ya que int sse declara, pero solo tiene que realizar un cálculo, es decir a + b. Por otro lado, el Método B tiene un menor uso de memoria, pero tiene que realizar dos cálculos, es decir, a + bdos veces. ¿Cuándo uso una técnica sobre la otra? O, ¿una de las técnicas siempre se prefiere sobre la otra? ¿Qué cosas hay que considerar al evaluar los dos métodos?

Método A:

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

Método B:

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}
Corey P
fuente
229
Estoy dispuesto a apostar que un compilador moderno generará el mismo ensamblaje para ambos casos.
17 de 26
12
Revertí la pregunta al estado original, ya que su edición anuló mi respuesta, ¡por favor no haga eso! Si hace una pregunta sobre cómo mejorar su código, no cambie la pregunta mejorando el código de la manera mostrada; esto hace que las respuestas parezcan sin sentido.
Doc Brown
76
Espera un segundo, ¿pidieron deshacerse int smientras estaban totalmente bien con esos números mágicos para los límites superior e inferior?
nulo
34
Recuerde: perfil antes de optimizar. Con los compiladores modernos, el Método A y el Método B pueden optimizarse para el mismo código (utilizando niveles de optimización más altos). Además, con los procesadores modernos, podrían tener instrucciones que realicen más que la suma en una sola operación.
Thomas Matthews
142
Ninguno; optimizar para facilitar la lectura.
Andy

Respuestas:

148

En lugar de especular sobre lo que puede o no suceder, veamos, ¿de acuerdo? Tendré que usar C ++ ya que no tengo un compilador de C # a mano (aunque vea el ejemplo de C # de VisualMelon ), pero estoy seguro de que los mismos principios se aplican independientemente.

Incluiremos las dos alternativas que encontró en la entrevista. También incluiremos una versión que utilice abssegún lo sugerido por algunas de las respuestas.

#include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}

Ahora compílelo sin optimización alguna: g++ -c -o test.o test.cpp

Ahora podemos ver exactamente lo que esto genera: objdump -d test.o

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   $0x3e8,-0x4(%rbp) # compare s to 1000
  1c:   7f 09                   jg     27                # jump to 27 if it's greater
  1e:   81 7d fc 18 fc ff ff    cmpl   $0xfffffc18,-0x4(%rbp) # compare s to -1000
  25:   7d 07                   jge    2e                # jump to 2e if it's greater or equal
  27:   b8 00 00 00 00          mov    $0x0,%eax         # put 0 (false) in eax, which will be the return value
  2c:   eb 05                   jmp    33 <_Z19IsSumInRangeWithVarii+0x33>
  2e:   b8 01 00 00 00          mov    $0x1,%eax         # put 1 (true) in eax
  33:   5d                      pop    %rbp
  34:   c3                      retq

0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
  35:   55                      push   %rbp
  36:   48 89 e5                mov    %rsp,%rbp
  39:   89 7d fc                mov    %edi,-0x4(%rbp)
  3c:   89 75 f8                mov    %esi,-0x8(%rbp)
  3f:   8b 55 fc                mov    -0x4(%rbp),%edx
  42:   8b 45 f8                mov    -0x8(%rbp),%eax  # same as before
  45:   01 d0                   add    %edx,%eax
  # note: unlike other implementation, result is not saved
  47:   3d e8 03 00 00          cmp    $0x3e8,%eax      # compare to 1000
  4c:   7f 0f                   jg     5d <_Z22IsSumInRangeWithoutVarii+0x28>
  4e:   8b 55 fc                mov    -0x4(%rbp),%edx  # since s wasn't saved, load a and b from the stack again
  51:   8b 45 f8                mov    -0x8(%rbp),%eax
  54:   01 d0                   add    %edx,%eax
  56:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax # compare to -1000
  5b:   7d 07                   jge    64 <_Z22IsSumInRangeWithoutVarii+0x2f>
  5d:   b8 00 00 00 00          mov    $0x0,%eax
  62:   eb 05                   jmp    69 <_Z22IsSumInRangeWithoutVarii+0x34>
  64:   b8 01 00 00 00          mov    $0x1,%eax
  69:   5d                      pop    %rbp
  6a:   c3                      retq

000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
  6b:   55                      push   %rbp
  6c:   48 89 e5                mov    %rsp,%rbp
  6f:   89 7d fc                mov    %edi,-0x4(%rbp)
  72:   89 75 f8                mov    %esi,-0x8(%rbp)
  75:   8b 55 fc                mov    -0x4(%rbp),%edx
  78:   8b 45 f8                mov    -0x8(%rbp),%eax
  7b:   01 d0                   add    %edx,%eax
  7d:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax
  82:   7c 16                   jl     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  84:   8b 55 fc                mov    -0x4(%rbp),%edx
  87:   8b 45 f8                mov    -0x8(%rbp),%eax
  8a:   01 d0                   add    %edx,%eax
  8c:   3d e8 03 00 00          cmp    $0x3e8,%eax
  91:   7f 07                   jg     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  93:   b8 01 00 00 00          mov    $0x1,%eax
  98:   eb 05                   jmp    9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
  9a:   b8 00 00 00 00          mov    $0x0,%eax
  9f:   5d                      pop    %rbp
  a0:   c3                      retq

Podemos ver en las direcciones de la pila (por ejemplo, el -0x4in mov %edi,-0x4(%rbp)versus el -0x14in mov %edi,-0x14(%rbp)) que IsSumInRangeWithVar()usa 16 bytes adicionales en la pila.

Debido a que IsSumInRangeWithoutVar()no asigna espacio en la pila para almacenar el valor intermedio s, tiene que volver a calcularlo, lo que hace que esta implementación tenga 2 instrucciones más.

Divertido, se IsSumInRangeSuperOptimized()parece mucho IsSumInRangeWithoutVar(), excepto que se compara con -1000 primero y 1000 segundo.

Ahora vamos a compilar con optimizaciones sólo las más básicas: g++ -O1 -c -o test.o test.cpp. El resultado:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    $0x7d0,%eax
   c:   0f 96 c0                setbe  %al
   f:   c3                      retq

0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
  10:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  17:   3d d0 07 00 00          cmp    $0x7d0,%eax
  1c:   0f 96 c0                setbe  %al
  1f:   c3                      retq

0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
  20:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  27:   3d d0 07 00 00          cmp    $0x7d0,%eax
  2c:   0f 96 c0                setbe  %al
  2f:   c3                      retq

¿Lo mirarías? Cada variante es idéntica . El compilador puede hacer algo bastante inteligente: abs(a + b) <= 1000es equivalente a a + b + 1000 <= 2000considerar setbehacer una comparación sin signo, por lo que un número negativo se convierte en un número positivo muy grande. La leainstrucción puede realizar todas estas adiciones en una sola instrucción y eliminar todas las ramas condicionales.

Para responder a su pregunta, casi siempre lo que hay que optimizar no es la memoria o la velocidad, sino la legibilidad . Leer el código es mucho más difícil que escribirlo, y leer el código que se ha manipulado para "optimizar" es mucho más difícil que leer el código que se ha escrito para que quede claro. La mayoría de las veces, estas "optimizaciones" tienen un impacto insignificante o, como en este caso, exactamente cero en el rendimiento real.


Pregunta de seguimiento, ¿qué cambia cuando este código está en un lenguaje interpretado en lugar de compilado? Entonces, ¿importa la optimización o tiene el mismo resultado?

¡Midamos! He transcrito los ejemplos a Python:

def IsSumInRangeWithVar(a, b):
    s = a + b
    if s > 1000 or s < -1000:
        return False
    else:
        return True

def IsSumInRangeWithoutVar(a, b):
    if a + b > 1000 or a + b < -1000:
        return False
    else:
        return True

def IsSumInRangeSuperOptimized(a, b):
    return abs(a + b) <= 1000

from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)

print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)

print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)

print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))

Ejecutar con Python 3.5.2, esto produce el resultado:

IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s

El desmontaje en Python no es terriblemente interesante, ya que el "compilador" de bytecode no hace mucho en cuanto a optimización.

El rendimiento de las tres funciones es casi idéntico. Podríamos sentir la tentación de ir IsSumInRangeWithVar()debido a su ganancia de velocidad marginal. Aunque agregaré, ya que estaba probando diferentes parámetros timeit, a veces IsSumInRangeSuperOptimized()salió más rápido, por lo que sospecho que pueden ser factores externos responsables de la diferencia, en lugar de cualquier ventaja intrínseca de cualquier implementación.

Si este es realmente un código crítico para el rendimiento, un lenguaje interpretado es simplemente una elección muy pobre. Al ejecutar el mismo programa con pypy, obtengo:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

El solo uso de pypy, que usa la compilación JIT para eliminar gran parte de la sobrecarga del intérprete, ha producido una mejora en el rendimiento de 1 o 2 órdenes de magnitud. Me sorprendió bastante ver que IsSumInRangeWithVar()es un orden de magnitud más rápido que los demás. Así que cambié el orden de los puntos de referencia y corrí nuevamente:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

¡Entonces parece que no es realmente nada acerca de la implementación lo que lo hace rápido, sino el orden en el que hago la evaluación comparativa!

Me encantaría profundizar más en esto, porque honestamente no sé por qué sucede esto. Pero creo que el punto ha sido señalado: las micro optimizaciones, como declarar un valor intermedio como variable o no, rara vez son relevantes. Con un lenguaje interpretado o un compilador altamente optimizado, el primer objetivo sigue siendo escribir código claro.

Si se requiere una mayor optimización, referencia . Recuerde que las mejores optimizaciones provienen no de los pequeños detalles sino de la imagen algorítmica más grande: pypy será un orden de magnitud más rápido para la evaluación repetida de la misma función que cpython porque usa algoritmos más rápidos (compilador JIT frente a interpretación) para evaluar el programa. Y también hay que considerar el algoritmo codificado: una búsqueda a través de un árbol B será más rápida que una lista vinculada.

Después de asegurarse de que está utilizando las herramientas y algoritmos adecuados para el trabajo, prepárese para profundizar en los detalles del sistema. Los resultados pueden ser muy sorprendentes, incluso para desarrolladores experimentados, y es por eso que debe tener un punto de referencia para cuantificar los cambios.

Phil Frost
fuente
66
Para proporcionar un ejemplo en C #: SharpLab produce un asm idéntico para ambos métodos (Desktop CLR v4.7.3130.00 (clr.dll) en x86)
VisualMelon
2
@VisualMelon es bastante divertido el cheque positivo: "return (((a + b)> = -1000) && ((a + b) <= 1000));" da un resultado diferente. : sharplab.io/…
Pieter B
12
La legibilidad también puede hacer que un programa sea más fácil de optimizar. El compilador puede reescribir fácilmente para usar una lógica equivalente como la anterior, solo si realmente puede descubrir lo que está tratando de hacer. Si usa muchos bithacks de la vieja escuela , va y viene entre entradas y punteros, reutiliza el almacenamiento mutable, etc., puede ser mucho más difícil para el compilador demostrar que una transformación es equivalente, y simplemente dejará lo que escribió , que puede ser subóptimo.
Leushenko
1
@Corey ver editar.
Phil Frost
2
@Corey: esta respuesta en realidad te dice exactamente lo que escribí en mi respuesta: no hay diferencia cuando usas un compilador decente y, en cambio, te enfocas en la disponibilidad. Por supuesto, se ve mejor fundado, tal vez me creas ahora.
Doc Brown
67

Para responder la pregunta planteada:

¿Cuándo optimizar la memoria frente a la velocidad de rendimiento para un método?

Hay dos cosas que debes establecer:

  • ¿Qué está limitando su aplicación?
  • ¿Dónde puedo recuperar la mayor parte de ese recurso?

Para responder a la primera pregunta, debe saber cuáles son los requisitos de rendimiento para su aplicación. Si no hay requisitos de rendimiento, entonces no hay razón para optimizar de una forma u otra. Los requisitos de rendimiento lo ayudan a llegar al lugar de "lo suficientemente bueno".

El método que proporcionó por sí solo no causaría ningún problema de rendimiento por sí solo, pero tal vez dentro de un ciclo y procesando una gran cantidad de datos, debe comenzar a pensar de manera un poco diferente sobre cómo aborda el problema.

Detectar lo que limita la aplicación.

Comience a observar el comportamiento de su aplicación con un monitor de rendimiento. Esté atento al uso de CPU, disco, red y memoria mientras se está ejecutando. Uno o más elementos se maximizarán mientras todo lo demás se usa moderadamente, a menos que alcance el equilibrio perfecto, pero eso casi nunca sucede).

Cuando necesita mirar más profundo, normalmente usaría un generador de perfiles . Hay perfiladores de memoria y perfiladores de procesos , y miden cosas diferentes. El acto de perfilar tiene un impacto significativo en el rendimiento, pero está instrumentando su código para descubrir qué está mal.

Digamos que ves que tu CPU y el uso del disco alcanzaron su punto máximo. Primero verificará si hay "puntos calientes" o código que se llama con más frecuencia que el resto o que requiere un porcentaje significativamente más largo del procesamiento.

Si no puede encontrar ningún punto caliente, entonces comenzará a mirar la memoria. Quizás esté creando más objetos de los necesarios y su recolección de basura esté trabajando horas extras.

Reclamando el rendimiento

Piensa críticamente. La siguiente lista de cambios está en orden de cuánto retorno de inversión obtendrá:

  • Arquitectura: buscar puntos de estrangulamiento de comunicación
  • Algoritmo: la forma en que procesas los datos puede necesitar cambiar
  • Puntos calientes: minimizar la frecuencia con la que se llama punto caliente puede generar una gran bonificación
  • Micro optimizaciones: no es común, pero a veces realmente necesita pensar en pequeños ajustes (como el ejemplo que proporcionó), particularmente si es un punto caliente en su código.

En situaciones como esta, debe aplicar el método científico. Presente una hipótesis, realice los cambios y pruébelo. Si cumple con sus objetivos de rendimiento, ya está. Si no, vaya a lo siguiente en la lista.


Respondiendo la pregunta en negrita:

¿Cuándo es apropiado usar el Método A versus el Método B y viceversa?

Honestamente, este es el último paso para tratar de lidiar con problemas de rendimiento o memoria. El impacto del Método A frente al Método B será realmente diferente según el idioma y la plataforma (en algunos casos).

Casi cualquier lenguaje compilado con un optimizador a mitad de camino generará un código similar con cualquiera de esas estructuras. Sin embargo, esas suposiciones no necesariamente siguen siendo ciertas en los lenguajes patentados y de juguete que no tienen un optimizador.

Precisamente, lo que tendrá un mejor impacto depende de si sumes una variable de pila o una variable de montón. Esta es una opción de implementación de lenguaje. En C, C ++ y Java, por ejemplo, las primitivas numéricas como un intson variables de pila por defecto. Su código no tiene más impacto en la memoria mediante la asignación a una variable de pila de la que tendría con un código totalmente integrado.

Otras optimizaciones que puede encontrar en las bibliotecas C (particularmente las más antiguas) en las que puede tener que decidir entre copiar una matriz bidimensional en primer lugar o en primer lugar es una optimización dependiente de la plataforma. Requiere cierto conocimiento de cómo el conjunto de chips al que se dirige optimiza mejor el acceso a la memoria. Hay diferencias sutiles entre arquitecturas.

La conclusión es que la optimización es una combinación de arte y ciencia. Requiere un poco de pensamiento crítico, así como un cierto grado de flexibilidad en la forma de abordar el problema. Busque cosas grandes antes de culpar a las cosas pequeñas.

Berin Loritsch
fuente
2
Esta respuesta se centra más en mi pregunta y no queda atrapada en mis ejemplos de codificación, es decir, Método A y Método B.
Corey P
18
Creo que esta es la respuesta genérica a "¿Cómo abordar los cuellos de botella de rendimiento", pero sería difícil identificar el uso de memoria relativa de una función particular en función de si tenía 4 o 5 variables con este método. También cuestiono cuán relevante es este nivel de optimización cuando el compilador (o el intérprete) puede o no optimizar esto.
Eric
@Eric, como mencioné, la última categoría de mejora del rendimiento serían sus microoptimizaciones. La única forma de adivinar si tendrá algún impacto es midiendo el rendimiento / la memoria en un generador de perfiles. Es raro que esos tipos de mejoras tengan beneficios, pero en los problemas de rendimiento sensibles al tiempo que tiene en los simuladores, un par de cambios bien colocados como ese pueden ser la diferencia entre alcanzar su objetivo de tiempo y no. Creo que puedo contar con una mano la cantidad de veces que valió la pena en más de 20 años de trabajo en software, pero no es cero.
Berin Loritsch
@BerinLoritsch Nuevamente, en general estoy de acuerdo con usted, pero en este caso específico no. He proporcionado mi propia respuesta, pero no he visto personalmente ninguna herramienta que marque o incluso le brinde formas de identificar potencialmente problemas de rendimiento relacionados con el tamaño de la memoria de la pila de una función.
Eric
@DocBrown, lo he remediado. En cuanto a la segunda pregunta, estoy bastante de acuerdo contigo.
Berin Loritsch
45

"esto reduciría la memoria" - em, no. Incluso si esto fuera cierto (que, para cualquier compilador decente no lo es), la diferencia probablemente sería insignificante para cualquier situación del mundo real.

Sin embargo, recomendaría usar el método A * (método A con un ligero cambio):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

pero por dos razones completamente diferentes:

  • Al dar a la variable sun nombre explicativo, el código se vuelve más claro

  • evita tener la misma lógica de suma dos veces en el código, por lo que el código se vuelve más SECO, lo que significa menos errores propensos a cambios.

Doc Brown
fuente
36
Lo limpiaría aún más e iría con "return sum> -1000 && sum <1000;".
17 de 26
36
@Corey cualquier optimizador decente usará un registro de CPU para la sumvariable, lo que conducirá a un uso de memoria cero. E incluso si no, esta es solo una palabra de memoria en un método de "hoja". Teniendo en cuenta cuán increíblemente derrochador de memoria Java o C # puede ser de otra manera debido a su GC y modelo de objeto, una intvariable local literalmente no utiliza ninguna memoria notable. Esta es una microoptimización sin sentido.
amon
10
@Corey: si es " un poco más complejo", probablemente no se convertirá en "un uso notable de memoria". Tal vez si construyes un ejemplo realmente más complejo, pero eso lo convierte en una pregunta diferente. Tenga en cuenta también que, debido a que no crea una variable específica para una expresión, para resultados intermedios complejos, el entorno de tiempo de ejecución aún puede crear internamente objetos temporales, por lo que depende completamente de los detalles del lenguaje, el entorno, el nivel de optimización y como se llame "notable".
Doc Brown
8
Además de los puntos anteriores, estoy bastante seguro de cómo C # / Java elige almacenar sumsería un detalle de implementación y dudo que alguien pueda hacer un caso convincente sobre si un truco tonto como evitar un local intconduciría a esto o esa cantidad de uso de memoria a largo plazo. La legibilidad de la OMI es más importante. La legibilidad puede ser subjetiva, pero FWIW, personalmente prefiero que nunca hagas el mismo cálculo dos veces, no para el uso de la CPU, sino porque solo tengo que inspeccionar tu adición una vez cuando estoy buscando un error.
jrh
2
... también tenga en cuenta que los idiomas recolectados de basura en general son un "mar de memoria agitado" impredecible que (para C # de todos modos) solo podría limpiarse cuando sea necesario , recuerdo haber hecho un programa que asignó gigabytes de RAM y solo comenzó " limpiando "después de sí mismo cuando la memoria escaseaba. Si el GC no necesita ejecutarse, puede llevar su tiempo dulce y guardar su CPU para asuntos más urgentes.
jrh
35

Puedes hacerlo mejor que los dos con

return (abs(a + b) > 1000);

La mayoría de los procesadores (y, por lo tanto, los compiladores) pueden hacer abs () en una sola operación. No solo tiene menos sumas, sino también menos comparaciones, que generalmente son más costosas desde el punto de vista computacional. También elimina la ramificación, que es mucho peor en la mayoría de los procesadores porque impide que sea posible la canalización.

El entrevistador, como han dicho otras respuestas, es la vida vegetal y no tiene por qué realizar una entrevista técnica.

Dicho esto, su pregunta es válida. Y la respuesta a cuándo optimiza y cómo, es cuándo ha demostrado que es necesario, y lo ha perfilado para demostrar exactamente qué partes lo necesitan . Knuth dijo que la optimización prematura es la raíz de todos los males, porque es demasiado fácil tratar de cubrir con oro secciones sin importancia, o hacer cambios (como su entrevistador) que no tienen ningún efecto, mientras se pierden los lugares que realmente lo necesitan. Hasta que tenga una prueba sólida, es realmente necesario, la claridad del código es el objetivo más importante.

Editar FabioTurati señala correctamente que este es el sentido lógico opuesto al original (¡mi error!), Y que esto ilustra un impacto adicional de la cita de Knuth, donde corremos el riesgo de romper el código mientras intentamos optimizarlo.

Graham
fuente
2
@ Corey, estoy bastante seguro de que Graham fija la solicitud "me retó a resolver el mismo problema con menos variables" como se esperaba. Si yo sería el entrevistador, yo esperaría que la respuesta, no se mueve a+ben ify hacerlo dos veces. Lo entiendes mal "Estaba contento y dijo que esto reduciría el uso de memoria por este método" - fue amable contigo, escondiendo su decepción con esta explicación sin sentido sobre la memoria. No deberías tomarte en serio hacer preguntas aquí. ¿Conseguiste un trabajo? Supongo que no :-(
Sinatr
1
Está aplicando 2 transformaciones al mismo tiempo: ha convertido las 2 condiciones en 1, utilizando abs(), y también tiene una sola return, en lugar de tener una cuando la condición es verdadera ("if branch") y otra cuando es falsa ( "rama más"). Cuando cambie un código como este, tenga cuidado: existe el riesgo de escribir inadvertidamente una función que devuelve verdadero cuando debería devolver falso, y viceversa. Que es exactamente lo que sucedió aquí. Sé que te estabas enfocando en otra cosa, y has hecho un buen trabajo al respecto. Aún así, esto podría fácilmente haberle costado el trabajo ...
Fabio Turati
2
@FabioTurati Bien visto - ¡gracias! Actualizaré la respuesta. Y es un buen punto sobre la refactorización y la optimización, lo que hace que la cita de Knuth sea aún más relevante. Deberíamos demostrar que necesitamos la optimización antes de correr el riesgo.
Graham
2
La mayoría de los procesadores (y, por lo tanto, los compiladores) pueden hacer abs () en una sola operación. Lamentablemente no es el caso de los enteros. ARM64 tiene una negación condicional que puede usar si los indicadores ya están configurados desde un adds, y ARM ha predicado reverse-sub ( rsblt= reverse-sub si less-tha) pero todo lo demás requiere múltiples instrucciones adicionales para implementar abs(a+b)o abs(a). godbolt.org/z/Ok_Con muestra la salida asm de x86, ARM, AArch64, PowerPC, MIPS y RISC-V. Solo mediante la transformación de la comparación en una comprobación de rango, (unsigned)(a+b+999) <= 1998Ugcc puede optimizarlo como en la respuesta de Phil.
Peter Cordes
2
El código "mejorado" en esta respuesta sigue siendo incorrecto, ya que produce una respuesta diferente para IsSumInRange(INT_MIN, 0). El código original regresa falseporque INT_MIN+0 > 1000 || INT_MIN+0 < -1000; pero el código "nuevo y mejorado" regresa trueporque abs(INT_MIN+0) < 1000. (O, en algunos idiomas, arrojará una excepción o tendrá un comportamiento indefinido. Revise sus listados locales.)
Quuxplusone,
16

¿Cuándo es apropiado usar el Método A versus el Método B y viceversa?

El hardware es barato; Los programadores son caros . Entonces, el costo del tiempo que ustedes dos perdieron en esta pregunta es probablemente mucho peor que cualquiera de las respuestas.

De todos modos, la mayoría de los compiladores modernos encontrarían una manera de optimizar la variable local en un registro (en lugar de asignar espacio de pila), por lo que los métodos son probablemente idénticos en términos de código ejecutable. Por esta razón, la mayoría de los desarrolladores elegirían la opción que comunique la intención más claramente (ver Escritura de código realmente obvio (ROC) ). En mi opinión, ese sería el Método A.

Por otro lado, si esto es puramente un ejercicio académico, puedes tener lo mejor de ambos mundos con el Método C:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}
John Wu
fuente
17
a+=bes un buen truco, pero tengo que mencionar (en caso de que no esté implícito en el resto de la respuesta), según mi experiencia, los métodos que alteran los parámetros pueden ser muy difíciles de depurar y mantener.
jrh
1
Estoy de acuerdo @jrh. Soy un gran defensor de la República de China, y ese tipo de cosas es todo lo contrario.
John Wu
3
"El hardware es barato; los programadores son caros". En el mundo de la electrónica de consumo, esa afirmación es falsa. Si vende millones de unidades, entonces es una muy buena inversión gastar $ 500,000 en costos de desarrollo adicionales para ahorrar $ 0,10 en los costos de hardware por unidad.
Bart van Ingen Schenau
2
@ JohnWu: simplificó el ifcheque, pero olvidó invertir el resultado de la comparación; su función ahora regresa truecuando noa + b está en el rango. O bien agregar una a la parte externa de la condición ( ), o distribuir las pruebas, invirtiendo, para obtener o para hacer que el flujo de prueba de alcance muy bien,!return !(a > 1000 || a < -1000)!return a <= 1000 && a >= -1000;return -1000 <= a && a <= 1000;
ShadowRanger
1
@JohnWu: Aún ligeramente en los casos extremos, la lógica distribuida requiere <=/ >=, no </ >(con </ >, 1000 y -1000 se tratan como fuera de rango, el código original los trata como dentro del rango).
ShadowRanger
11

Me gustaría optimizar la legibilidad. Método X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

Pequeños métodos que hacen solo 1 cosa pero son fáciles de razonar.

(Esta es una preferencia personal, me gustan las pruebas positivas en lugar de negativas, su código original realmente está probando si el valor NO está fuera del rango).

Pieter B
fuente
55
Esta. (Los comentarios votados arriba que fueron similares en re: legibilidad). Hace 30 años, cuando trabajábamos con máquinas que tenían menos de 1 MB de RAM, era necesario reducir el rendimiento, al igual que el problema de y2k, obtener unos cientos de miles de registros en los que cada uno tiene unos pocos bytes de memoria desperdiciados debido a vars no utilizados y referencias, etc. y se acumula rápidamente cuando solo tienes 256k de RAM. Ahora que estamos tratando con máquinas que tienen múltiples gigabytes de RAM, ahorrar incluso unos pocos MB de uso de RAM frente a la legibilidad y la facilidad de mantenimiento del código no es un buen negocio.
ivanivan
@ivanivan: No creo que el "problema y2k" fuera realmente sobre la memoria. Desde el punto de vista del ingreso de datos, ingresar dos dígitos es más eficiente que ingresar cuatro, y mantener las cosas como ingresadas es más fácil que convertirlas a alguna otra forma.
supercat
10
Ahora tiene que rastrear 2 funciones para ver qué sucede. No puede tomarlo al pie de la letra, porque no puede distinguir por el nombre si estos son límites inclusivos o exclusivos. Y si agrega esa información, el nombre de la función es más largo que el código para expresarla.
Peter
1
Optimice la legibilidad y cree funciones pequeñas y fáciles de razonar: claro, de acuerdo. Pero estoy totalmente en desacuerdo de que el cambio de nombre ay bde number1y number2ayudas para la legibilidad de ninguna manera. Además, su denominación de las funciones es inconsistente: ¿por qué IsSumInRangecodifica el rango de forma rígida si lo IsValueInRangeacepta como argumentos?
Leftaroundabout
La primera función puede desbordarse. (Al igual que el código de otras respuestas). Aunque la complejidad del código seguro de desbordamiento es un argumento para ponerlo en una función.
philipxy
6

En resumen, no creo que la pregunta tenga mucha relevancia en la informática actual, pero desde una perspectiva histórica es un ejercicio de pensamiento interesante.

Es probable que su entrevistador sea fanático del Mes del Hombre Mítico. En el libro, Fred Brooks expone que los programadores generalmente necesitarán dos versiones de funciones clave en su caja de herramientas: una versión optimizada para la memoria y una versión optimizada para la CPU. Fred basó esto en su experiencia al liderar el desarrollo del sistema operativo IBM System / 360 donde las máquinas pueden tener tan solo 8 kilobytes de RAM. En tales máquinas, la memoria requerida para las variables locales en las funciones podría ser importante, especialmente si el compilador no las optimizó de manera efectiva (o si el código se escribió directamente en lenguaje ensamblador).

En la era actual, creo que sería difícil encontrar un sistema donde la presencia o ausencia de una variable local en un método marcara una diferencia notable. Para que una variable importe, el método debería ser recursivo con una recursión profunda esperada. Incluso entonces, es probable que se exceda la profundidad de la pila causando excepciones de desbordamiento de pila antes de que la variable en sí misma cause un problema. El único escenario real en el que puede ser un problema es con matrices muy grandes asignadas en la pila en un método recursivo. Pero eso también es poco probable, ya que creo que la mayoría de los desarrolladores pensarían dos veces antes de las copias innecesarias de matrices grandes.

Eric
fuente
4

Después de la asignación s = a + b; las variables ayb ya no se usan. Por lo tanto, no se utiliza memoria para s si no está utilizando un compilador con daño cerebral completo; la memoria que se utilizó de todos modos para a y b se reutiliza.

Pero optimizar esta función es completamente absurdo. Si pudiera ahorrar espacio, serían quizás 8 bytes mientras la función se está ejecutando (que se recupera cuando la función regresa), por lo que es absolutamente inútil. Si pudiera ahorrar tiempo, serían números únicos de nanosegundos. Optimizar esto es una pérdida total de tiempo.

gnasher729
fuente
3

Las variables de tipo de valor local se asignan en la pila o (lo más probable es que para piezas de código tan pequeñas) utilicen registros en el procesador y nunca lleguen a ver RAM. De cualquier manera, son de corta duración y no hay nada de qué preocuparse. Comienza a considerar el uso de la memoria cuando necesita almacenar en búfer o poner en cola elementos de datos en colecciones que son potencialmente grandes y de larga duración.

Entonces, depende de lo que más le interese para su aplicación. ¿Velocidad de procesamiento? ¿Tiempo de respuesta? Huella de memoria? Mantenibilidad? ¿Consistencia en el diseño? Todo depende de ti.

Martin Maat
fuente
44
Nitpicking: .NET al menos (el idioma de la publicación no está especificado) no garantiza que las variables locales se asignen "en la pila". Ver "la pila es un detalle de implementación" por Eric Lippert.
jrh
1
@jrh Las variables locales en la pila o el montón pueden ser un detalle de implementación, pero si alguien realmente quería una variable en la pila, existe stackallocy ahora Span<T>. Posiblemente útil en un punto caliente, después de perfilar. Además, algunos de los documentos sobre estructuras implican que los tipos de valor pueden estar en la pila, mientras que los tipos de referencia no lo estarán. De todos modos, en el mejor de los casos, puede evitar un poco de GC.
Bob
2

Como han dicho otras respuestas, debes pensar para qué estás optimizando.

En este ejemplo, sospecho que cualquier compilador decente generaría un código equivalente para ambos métodos, por lo que la decisión no tendría ningún efecto en el tiempo de ejecución o la memoria.

Lo que afecta es la legibilidad del código. (El código es para que los humanos lo lean, no solo las computadoras). No hay demasiada diferencia entre los dos ejemplos; cuando todas las demás cosas son iguales, considero que la brevedad es una virtud, por lo que probablemente elegiría el Método B. Pero todas las demás cosas rara vez son iguales, y en un caso más complejo del mundo real, podría tener un gran efecto.

Cosas para considerar:

  • ¿La expresión intermedia tiene algún efecto secundario? Si llama a funciones impuras o actualiza alguna variable, entonces, por supuesto, duplicarlo sería una cuestión de corrección, no solo de estilo.
  • ¿Qué tan compleja es la expresión intermedia? Si hace muchos cálculos y / o llama a funciones, entonces el compilador puede no ser capaz de optimizarlo, y esto afectaría el rendimiento. (Sin embargo, como dijo Knuth , "Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo").
  • ¿La variable intermedia tiene algún significado ? ¿Podría recibir un nombre que ayude a explicar lo que está pasando? Un nombre corto pero informativo podría explicar mejor el código, mientras que uno sin sentido es solo ruido visual.
  • ¿Cuánto dura la expresión intermedia? Si es largo, duplicarlo podría hacer que el código sea más largo y difícil de leer (especialmente si fuerza un salto de línea); si no, la duplicación podría ser más corta en general.
gidds
fuente
1

Como muchas de las respuestas han señalado, intentar ajustar esta función con compiladores modernos no hará ninguna diferencia. Es muy probable que un optimizador encuentre la mejor solución (¡vote por la respuesta que mostró el código del ensamblador para probarlo!). Usted dijo que el código en la entrevista no era exactamente el código que le pidieron comparar, por lo que quizás el ejemplo real tenga un poco más de sentido.

Pero echemos otro vistazo a esta pregunta: esta es una pregunta de entrevista. Entonces, el verdadero problema es, ¿cómo debería responderlo asumiendo que desea intentar conseguir el trabajo?

Supongamos también que el entrevistador sabe de lo que está hablando y solo está tratando de ver lo que usted sabe.

Mencionaría que, ignorando el optimizador, el primero puede crear una variable temporal en la pila mientras que el segundo no lo haría, pero realizaría el cálculo dos veces. Por lo tanto, el primero usa más memoria pero es más rápido.

Podría mencionar que, de todos modos, un cálculo puede requerir una variable temporal para almacenar el resultado (para que se pueda comparar), por lo tanto, si nombra esa variable o no, puede no haber ninguna diferencia.

Luego mencionaría que en realidad el código se optimizaría y probablemente se generaría un código de máquina equivalente, ya que todas las variables son locales. Sin embargo, depende del compilador que esté utilizando (no hace mucho tiempo pude obtener una mejora útil del rendimiento al declarar una variable local como "final" en Java).

Podría mencionar que, en cualquier caso, la pila vive en su propia página de memoria, por lo que, a menos que su variable adicional haga que la pila desborde la página, en realidad no asignará más memoria. Sin embargo, si se desborda, querrá una página completamente nueva.

Mencionaría que un ejemplo más realista podría ser la elección de si usar un caché para contener los resultados de muchos cálculos o no, y esto plantearía una cuestión de CPU vs memoria.

Todo esto demuestra que sabes de lo que estás hablando.

Lo dejaría hasta el final para decir que sería mejor centrarse en la legibilidad en su lugar. Aunque es cierto en este caso, en el contexto de la entrevista puede interpretarse como "No sé sobre el rendimiento, pero mi código se lee como una historia de Janet y John ".

Lo que no debe hacer es trotear las declaraciones insípidas habituales sobre cómo no es necesaria la optimización del código, no optimice hasta que haya perfilado el código (esto solo indica que no puede ver el código incorrecto), el hardware cuesta menos que los programadores , y por favor, por favor, no cites a Knuth "prematuro, bla, bla ...".

El rendimiento del código es un problema genuino en muchas organizaciones y muchas organizaciones necesitan programadores que lo entiendan.

En particular, con organizaciones como Amazon, parte del código tiene una gran influencia. Se puede implementar un fragmento de código en miles de servidores o millones de dispositivos y se puede llamar miles de millones de veces al día todos los días del año. Puede haber miles de fragmentos similares. La diferencia entre un algoritmo malo y uno bueno puede ser fácilmente un factor de mil. Haga los números y multiplique todo esto: hace la diferencia. El costo potencial para la organización del código que no funciona puede ser muy significativo o incluso fatal si un sistema se queda sin capacidad.

Además, muchas de estas organizaciones trabajan en un entorno competitivo. Por lo tanto, no puede simplemente decirles a sus clientes que compren una computadora más grande si el software de su competencia ya funciona bien en el hardware que tienen o si el software se ejecuta en un teléfono móvil y no se puede actualizar. Algunas aplicaciones son particularmente críticas para el rendimiento (me vienen a la mente juegos y aplicaciones móviles) y pueden vivir o morir de acuerdo con su capacidad de respuesta o velocidad.

Personalmente, durante más de dos décadas, trabajé en muchos proyectos en los que los sistemas fallaron o no se pudieron usar debido a problemas de rendimiento y me llamaron para optimizar esos sistemas y en todos los casos se debió a un código incorrecto escrito por programadores que no entendieron El impacto de lo que estaban escribiendo. Además, nunca es un código, siempre está en todas partes. Cuando aparezco, es demasiado tarde para comenzar a pensar en el rendimiento: el daño ya está hecho.

Comprender el rendimiento del código es una buena habilidad para tener de la misma manera que comprender la corrección del código y el estilo del código. Sale de la práctica. Las fallas de rendimiento pueden ser tan malas como las fallas funcionales. Si el sistema no funciona, no funciona. No importa por qué. Del mismo modo, el rendimiento y las características que nunca se usan son malos.

Por lo tanto, si el entrevistador le pregunta sobre el desempeño, le recomendaría que pruebe y demuestre el mayor conocimiento posible. Si la pregunta parece mala, explique cortésmente por qué cree que no sería un problema en ese caso. No cites a Knuth.

rghome
fuente
0

Primero debe optimizar la corrección.

Su función falla para los valores de entrada que están cerca de Int.MaxValue:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

Esto devuelve verdadero porque la suma se desborda a -400. La función tampoco funciona para a = int.MinValue + 200. (incorrectamente se suma a "400")

No sabremos qué estaba buscando el entrevistador a menos que intervenga, pero "el desbordamiento es real" .

En una situación de entrevista, haga preguntas para aclarar el alcance del problema: ¿Cuáles son los valores de entrada máximos y mínimos permitidos? Una vez que los tenga, puede lanzar una excepción si la persona que llama envía valores fuera del rango. O (en C #), puede usar una sección marcada {}, que generaría una excepción en caso de desbordamiento. Sí, es más trabajo y complicado, pero a veces eso es lo que se necesita.

TomEberhard
fuente
Los métodos fueron solo ejemplos. No fueron escritos para ser correctos, sino para ilustrar la pregunta real. Gracias por la entrada sin embargo!
Corey P
Creo que la pregunta de la entrevista está dirigida al rendimiento, por lo que debe responder la intención de la pregunta. El entrevistador no pregunta sobre el comportamiento en los límites. Pero punto lateral interesante de todos modos.
rghome
1
@Corey Good entrevistadores como preguntas para 1) evaluar la capacidad del candidato con respecto al tema, como lo sugiere rghome aquí pero también 2) como una apertura a los problemas más grandes (como la corrección funcional tácita) y la profundidad del conocimiento relacionado: esto es más en entrevistas de carrera posteriores, buena suerte.
chux
0

Su pregunta debería haber sido: "¿Necesito optimizar esto en absoluto?".

Las versiones A y B difieren en un detalle importante que hace que A sea preferible, pero no está relacionado con la optimización: no repites el código.

La "optimización" real se llama eliminación de subexpresión común, que es lo que hacen prácticamente todos los compiladores. Algunos hacen esta optimización básica incluso cuando las optimizaciones están desactivadas. Por lo tanto, eso no es realmente una optimización (el código generado seguramente será exactamente el mismo en todos los casos).

Pero si no es una optimización, ¿por qué es preferible? Muy bien, no repites el código, ¡a quién le importa!

Bueno, antes que nada, no corre el riesgo de equivocarse accidentalmente con la mitad de la cláusula condicional. Pero lo que es más importante, alguien que lea este código puede entender inmediatamente lo que está tratando de hacer, en lugar de una if((((wtf||is||this||longexpression))))experiencia. Lo que el lector puede ver es if(one || theother), que es algo bueno. No rara vez, sucede que usted es esa otra persona que lee su propio código tres años después y piensa "¿Qué significa esto?". En ese caso, siempre es útil si su código comunica inmediatamente cuál fue la intención. Con una subexpresión común nombrada correctamente, ese es el caso.
Además, si en cualquier momento en el futuro, por ejemplo, decide que necesita cambiar a+ba a-b, usted tiene que cambiar unoubicación, no dos. Y no hay riesgo de que (de nuevo) se equivoque el segundo por accidente.

Acerca de su pregunta real, para qué debe optimizar, en primer lugar, su código debe ser correcto . Esto es lo más importante. El código que no es correcto es un código incorrecto, incluso más si a pesar de ser incorrecto "funciona bien", o al menos parece que funciona bien. Después de eso, el código debe ser legible (legible por alguien que no esté familiarizado con él).
En cuanto a la optimización ... uno ciertamente no debería escribir deliberadamente código anti-optimizado, y ciertamente no estoy diciendo que no deba pensar en el diseño antes de comenzar (como elegir el algoritmo correcto para el problema, no el menos eficiente).

Pero para la mayoría de las aplicaciones, la mayoría de las veces, el rendimiento que obtienes después de ejecutar un código correcto y legible utilizando un algoritmo razonable a través de un compilador de optimización está bien, no hay necesidad real de preocuparse.

Si ese no es el caso, es decir, si el rendimiento de la aplicación no cumple con los requisitos, y solo entonces , debe preocuparse por hacer optimizaciones locales como la que intentó. Preferiblemente, sin embargo, reconsideraría el algoritmo de nivel superior. Si llama a una función 500 veces en lugar de 50,000 veces debido a un algoritmo mejor, esto tiene un mayor impacto que guardar tres ciclos de reloj en una microoptimización. Si no se detiene durante varios cientos de ciclos en un acceso aleatorio a la memoria todo el tiempo, esto tiene un impacto mayor que hacer algunos cálculos extra baratos, etc.

La optimización es un asunto difícil (puedes escribir libros completos sobre eso y no tener fin), y pasar tiempo optimizando ciegamente un punto en particular (¡sin siquiera saber si ese es el cuello de botella!) Generalmente es tiempo perdido. Sin perfiles, la optimización es muy difícil de acertar.

Pero como regla general, cuando vuelas ciego y solo necesitas / quieres hacer algo , o como una estrategia general predeterminada, sugeriría optimizar para "memoria".
La optimización para la "memoria" (en particular, la ubicación espacial y los patrones de acceso) generalmente produce un beneficio porque, a diferencia de lo que alguna vez fue "casi igual", hoy en día acceder a la RAM es una de las cosas más caras (¡menos leer desde el disco!) que en principio puedes hacer. Mientras que ALU, por otro lado, es barato y cada vez es más rápido. El ancho de banda y la latencia de la memoria no mejoran tan rápido. La buena localidad y los buenos patrones de acceso pueden hacer fácilmente una diferencia de 5x (20x en ejemplos extremos y elaborados) en tiempo de ejecución en comparación con los malos patrones de acceso en aplicaciones con muchos datos. Sé amable con tus cachés, y serás una persona feliz.

Para poner el párrafo anterior en perspectiva, considere lo que le cuestan las diferentes cosas que puede hacer. Ejecutar algo como a+btoma (si no está optimizado) uno o dos ciclos, pero la CPU generalmente puede iniciar varias instrucciones por ciclo y puede canalizar instrucciones no dependientes, de manera más realista, solo le cuesta algo alrededor de medio ciclo o menos. Idealmente, si el compilador es bueno programando, y dependiendo de la situación, puede costar cero.
Obtener datos ("memoria") le cuesta entre 4 y 5 ciclos si tiene suerte y está en L1, y alrededor de 15 ciclos si no tiene tanta suerte (golpe L2). Si los datos no están en la memoria caché, se necesitan varios cientos de ciclos. Si su patrón de acceso aleatorio excede las capacidades de TLB (fácil de hacer con solo ~ 50 entradas), agregue otros cientos de ciclos. Si su patrón de acceso aleatorio en realidad causa un error de página, le costará unos diez mil ciclos en el mejor de los casos, y varios millones en el peor.
Ahora piénselo, ¿qué es lo que quiere evitar con más urgencia?

Damon
fuente
0

¿Cuándo optimizar la memoria frente a la velocidad de rendimiento para un método?

Después de obtener la funcionalidad correcta primero . Entonces la selectividad se preocupa por las micro optimizaciones.


Como una pregunta de la entrevista con respecto a las optimizaciones, el código provoca la discusión habitual pero pierde el objetivo de nivel superior de ¿Es el código funcionalmente correcto?

Tanto C ++ como C y otros consideran el intdesbordamiento como un problema de a + b. No está bien definido y C lo llama comportamiento indefinido . No se especifica para "envolver", aunque ese es el comportamiento común.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

IsSumInRange()Se esperaría que dicha función llamada esté bien definida y funcione correctamente para todos los intvalores de a,b. Lo crudo a + bno lo es. La solución de CA podría usar:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

El código de seguridad puede ser optimizado mediante el uso de un tipo entero más ancha que int, si está disponible, como a continuación o distribuir el sum > N, sum < -Npruebas dentro de la if (a >= 0)lógica. Sin embargo, tales optimizaciones pueden no conducir realmente a un código emitido "más rápido" dado un compilador inteligente ni valer el mantenimiento adicional de ser inteligente.

  long long sum a;
  sum += b;

Incluso el uso abs(sum)es propenso a problemas cuando sum == INT_MIN.

chux
fuente
0

¿De qué tipo de compiladores estamos hablando y de qué tipo de "memoria"? Porque en su ejemplo, suponiendo un optimizador razonable, la expresión a+bgeneralmente debe almacenarse en un registro (una forma de memoria) antes de realizar dicha aritmética.

Entonces, si estamos hablando de un compilador tonto que se encuentra a+bdos veces, va a asignar más registros (memoria) en su segundo ejemplo, porque su primer ejemplo podría almacenar esa expresión una vez en un solo registro asignado a la variable local, pero nosotros estamos hablando de compiladores muy tontos en este punto ... a menos que esté trabajando con otro tipo de compilador tonto que la pila derrame todas las variables por todas partes, en cuyo caso tal vez la primera causaría más dificultades para optimizar que el segundo*.

Todavía quiero descartar eso y creo que el segundo probablemente usará más memoria con un compilador tonto, incluso si es propenso a acumular derrames, porque podría terminar asignando tres registros a+by derrames ay bmás. Si estamos hablando optimizador más primitivo luego capturar a+ba sprobablemente "ayuda" que utilizan menos registros / derrames de pila.

Todo esto es extremadamente especulativo en formas bastante tontas en ausencia de mediciones / desmontaje e incluso en el peor de los casos, este no es un caso de "memoria versus rendimiento" (porque incluso entre los peores optimizadores que puedo pensar, no estamos hablando sobre cualquier cosa que no sea memoria temporal como pila / registro), es un caso de "rendimiento" en el mejor de los casos, y entre cualquier optimizador razonable, los dos son equivalentes, y si uno no está usando un optimizador razonable, ¿por qué se obsesiona con la optimización de naturaleza tan microscópica y mediciones especialmente ausentes? Es como un enfoque de nivel de ensamblaje de selección de instrucciones / asignación de registros que nunca esperaría que alguien que buscara ser productivo cuando usa, por ejemplo, un intérprete que acumula todo.

¿Cuándo optimizar la memoria frente a la velocidad de rendimiento para un método?

En cuanto a esta pregunta, si puedo abordarla de manera más amplia, a menudo no encuentro los dos diametralmente opuestos. Especialmente si sus patrones de acceso son secuenciales, y dada la velocidad de la memoria caché de la CPU, a menudo una reducción en la cantidad de bytes procesados ​​secuencialmente para entradas no triviales se traduce (hasta cierto punto) para explorar esos datos más rápido. Por supuesto, hay puntos de ruptura en los que si los datos son mucho, mucho más pequeños a cambio de muchas más instrucciones, podría ser más rápido procesar secuencialmente en forma más grande a cambio de menos instrucciones.

Pero he descubierto que muchos desarrolladores tienden a subestimar cuánto puede reducir una reducción en el uso de la memoria en este tipo de casos a reducciones proporcionales en el tiempo dedicado al procesamiento. Es muy humano intuitivo traducir los costos de rendimiento en instrucciones en lugar de acceder a la memoria hasta el punto de alcanzar grandes LUT en un intento vano de acelerar algunos cálculos pequeños, solo para encontrar un rendimiento degradado con el acceso adicional a la memoria.

Para los casos de acceso secuencial a través de una gran matriz (sin hablar de variables escalares locales como en su ejemplo), sigo la regla de que menos memoria para pasar secuencialmente se traduce en un mayor rendimiento, especialmente cuando el código resultante es más simple que lo contrario, hasta que no t, hasta que mis mediciones y mi perfilador me digan lo contrario, y es importante, de la misma manera que supongo que leer secuencialmente un archivo binario más pequeño en el disco sería más rápido de leer que uno más grande (incluso si el más pequeño requiere más instrucciones) ), hasta que se demuestre que esa suposición ya no se aplica en mis mediciones.

Dragon Energy
fuente