En ya se ha dicho Big O no mide la velocidad, sino la capacidad de escalado. Por eso es muy útil para comparar algoritmos de diferentes clases de escalado. Por ejemplo, para grandes conjuntos de datos, un algoritmo loglineal ( $\mathcal{O}(n\log n)$ ) (por ejemplo, quicksort) superará a una ordenación cuadrática ( $\mathcal{O}(n^2)$ ) (por ejemplo, ordenación por inserción). Por lo tanto, las personas que tienen que manejar grandes conjuntos de datos utilizan la ordenación rápida en lugar de la ordenación por inserción. Pero Big O no te dice si la ordenación por inserción es más rápida que la ordenación burbuja. Y sobre todo no te dice lo bien que manejará tus datos. La ordenación por burbujas puede ser excelente para ordenar datos casi ordenados. Hace la validación en tiempo lineal. La ordenación por inserción es más simple y rápida que la ordenación rápida para pequeñas cantidades de datos.
En la pregunta, usted dijo
Supongamos que tenemos un algoritmo que se ejecuta en $1000n$ pasos.
En realidad, es bastante difícil de calcular.
Considera que tengo una variable en mi programa que es accesible globalmente. Le doy un valor. Hago un montón de cosas, suficientes para que la variable sólo se almacene en RAM, no en caché ni en un registro. Quiero añadir uno a esa variable. ¿Cuántos pasos son?
Podrías señalar que existe literalmente una instrucción para añadir uno a una variable (supongamos que existe una para esa plataforma). Pero esa instrucción asume que la variable está en un registro. Así que necesitamos cargar la variable en un registro. Tenemos una instrucción para eso. Pero esa instrucción asume que la variable está en caché. Ignorando los diferentes niveles de caché (y la paginación a disco), necesitamos cargar la variable desde la RAM. Pero resulta que en realidad no tenemos una instrucción para cargar una sola variable de la RAM a la caché (y mucho menos a un registro). Lo que tenemos es la capacidad de cargar un bloque de RAM en la caché.
Vale, esto es más complicado de lo que parecía al principio. Pero seguimos jugando. Averiguamos qué bloque de memoria contiene la variable. Eso puede llevar algunos pasos propios, digamos cinco. Entonces emitimos la instrucción de carga. Ahora descubrimos que, como la RAM es más lenta que la caché, esta instrucción tarda varios pasos en terminar. Algo así como empezar a cargar, noventa y ocho pasos de espera, terminar de cargar. Si tenemos otro trabajo, podemos hacer eso en lugar de los noventa y ocho pasos de espera. Pero asumamos que no podemos hacer ningún otro trabajo hasta que tengamos esta variable incrementada.
Finalmente tenemos el valor de la variable cargado en caché. Ahora necesitamos emitir otro comando para cargarlo desde la caché a un registro. Así que lo hacemos. Resulta que eso lleva otros diez pasos de tiempo. Una vez en el registro, podemos incrementarlo. Hay 116 pasos para hacer una línea de código. Y esa era una línea de código realmente simple.
Una vez que tenemos el valor incrementado, podemos utilizarlo para lo que necesitábamos. Vamos a calcular otros diez pasos. Ahora, queremos incrementarlo de nuevo. ¿Necesitamos otros 116 pasos? No, ya está en el registro. Podemos hacer la misma instrucción en un solo paso. Así que hemos incrementado el valor dos veces en sólo 127 pasos. De los cuales 115 fueron sólo para llegar al punto de partida. Pero desde la perspectiva del código que escribimos, esa fue la misma instrucción de alto nivel. Entonces, ¿cómo convertimos el código de alto nivel en pasos?
No podemos limitarnos a contar líneas de código. Algunas líneas requerirán más trabajo que otras. Ni siquiera podemos estar seguros de cuántos pasos pueden llevar líneas de código idénticas. A veces un simple incremento puede tomar un paso. A veces 116. (Es realmente difícil contar el número de pasos en código compilado o interpretado. Tan difícil que rara vez lo intentamos. Es mucho más probable que utilicemos un perfilador o un benchmarking para medir el tiempo que tarda en ejecutarse un programa.
Pero a Big O no le importa. Sólo asume que el tiempo será relativamente consistente. De tal manera que duplicar el tamaño de los datos puede duplicar el tiempo que tarda en ejecutarse. O cuadruplicarlo. O aumentarlo un 40%. O triplicarlo. Por lo general, será más rápido utilizar la O grande más pequeña. Y para los casos en los que no lo sea, podemos recurrir a la creación de perfiles y a la evaluación comparativa. Pero ha sido muy raro que haya intentado contar el número de pasos, y nunca profesionalmente. Es el tipo de análisis que se suele hacer en el mundo académico con fines ilustrativos. En realidad, no funciona en la programación real. Porque los compiladores y las CPU son mucho más complicados que el simple procesamiento de pasos.
De hecho, son más complicados que este ejemplo. En concreto, la idea de "pasos de espera" entre los pasos de inicio y fin es algo cuestionable. Pero esa ilustración facilita la reflexión sobre lo que ocurre, aunque no sea una descripción precisa de lo que ocurre en realidad.
Big O ayuda principalmente a que los programadores eviten los verdaderos errores, como calcular la secuencia de Fibonacci recursivamente como un árbol binario. Tenemos una solución de tiempo lineal bastante sencilla, una solución de tiempo constante más complicada para un único valor de la secuencia y una solución recursiva elegante pero de tiempo exponencial. No se me ocurre ninguna circunstancia en la que queramos la solución de tiempo exponencial (aparte de demostrar que es lenta). Pero tanto la solución lineal como la de tiempo constante podrían ser óptimas para un caso de uso determinado.
Si realmente necesitas optimizar un programa, no puedes detenerte con Big O. Necesitas continuar con la creación de perfiles y la evaluación comparativa. Big O ayuda a evitar algunos grandes errores. Pero hay muchos errores que nunca detectará.