web-dev-qa-db-fra.com

Pourquoi utiliser une boucle pour itérer du début à la fin d'un tableau plus rapidement que d'itérer début-fin et fin-début?

Etant donné un tableau ayant .length100 contenant des éléments ayant les valeurs 0 à 99 aux index respectifs, où il est nécessaire de trouver un élément du tableau égal à n: 51

Pourquoi utiliser une boucle pour itérer du début à la fin d'un tableau plus rapidement que d'itérer à la fois début-fin et fin-début?

const arr = Array.from({length: 100}, (_, i) => i);
const n = 51;
const len = arr.length;

console.time("iterate from start");
for (let i = 0; i < len; i++) {
  if (arr[i] === n) break;
}
console.timeEnd("iterate from start");

const arr = Array.from({length: 100}, (_, i) => i);
const n = 51;
const len = arr.length;

console.time("iterate from start and end");
for (let i = 0, k = len - 1; i < len && k >= 0; i++, k--) {
  if (arr[i] === n || arr[k] === n) break;
}
console.timeEnd("iterate from start and end");

jsperf https://jsperf.com/iterate-from-start-iterate-from-start-and-end/1

21
guest271314

La réponse est assez évidente:

Plus d'opérations prennent plus de temps.

Lorsque vous évaluez la vitesse du code, vous déterminez le nombre d'opérations qu'il effectuera. Il suffit de les traverser et de les compter. Chaque instruction prendra un ou plusieurs cycles de la CPU, et plus il faudra de temps pour l'exécuter. Peu importe que des instructions différentes prennent un nombre différent de cycles - une recherche de tableau peut être plus coûteuse qu'une arithmétique entière, mais les deux prennent un temps constant et si elles sont trop nombreuses, elles dominent le coût de notre algorithme.

Dans votre exemple, il existe quelques types d'opérations que vous pouvez compter individuellement:

  • des comparaisons
  • incréments/décréments
  • recherche de tableau
  • sauts conditionnels

(nous pourrions être plus granulaires, comme compter les opérations d'extraction et de stockage de variables, mais celles-ci importent peu - tout est dans les registres de toute façon - et leur nombre est fondamentalement linéaire par rapport aux autres).

Maintenant, vos deux codes itèrent environ 50 fois - leur élément sur lequel ils cassent la boucle se trouve au milieu du tableau. Ignorant quelques erreurs, voici les comptes:

               |  forwards  | forwards and backwards
---------------+------------+------------------------
>=/===/<       |       100  |                   200
++/--          |        50  |                   100
a[b]           |        50  |                   100
&&/||/if/for   |       100  |                   200

Etant donné cela, il est pas surprenant que faire deux fois le travail prend beaucoup plus de temps.

Je répondrai également à quelques questions de vos commentaires:

Un délai supplémentaire est-il nécessaire pour la deuxième recherche d'objet?

Oui, chaque recherche individuelle compte. Ce n'est pas comme si elles pouvaient être exécutées à la fois ou optimisées en une seule recherche (imaginable si elles avaient recherché le même index).

Devrait-il y avoir deux boucles distinctes pour chaque début et fin pour commencer?

Peu importe le nombre d'opérations, juste leur ordre.

Ou, autrement dit, quelle est l’approche la plus rapide pour trouver un élément dans un tableau?

Il n'y a pas de "plus rapide" en ce qui concerne l'ordre. Si vous ne savez pas où se trouve l'élément (et qu'ils sont répartis de manière égale), vous devez essayer tous les index. Tout ordre, même aléatoire, fonctionnerait de la même manière. Notez cependant que votre code est strictement pire, car il examine chaque index deux fois lorsque l'élément n'est pas trouvé - il ne s'arrête pas au milieu.

Néanmoins, il existe quelques approches différentes pour la micro-optimisation d'une telle boucle - vérifier ces repères .

39
Bergi

@ Bergi est correct. Plus d'opérations, c'est plus de temps. Pourquoi? Plus de cycles d'horloge du processeur . Le temps est en réalité une référence au nombre de cycles d'horloge nécessaires à l'exécution du code . Pour obtenir tous ces détails, vous devez examiner le code au niveau de la machine (comme Code de niveau d'assemblage) pour trouver la preuve réelle. Chaque cycle d'horloge de la CPU (noyau?) Peut exécuter une instruction. Combien d'instructions exécutez-vous?

Je n'ai pas compté le nombre de cycles d'horloge depuis la programmation de processeurs Motorola pour des applications intégrées. Si votre code prend plus de temps, il génère en fait un plus grand jeu d'instructions de code machine, même si la boucle est plus courte ou s'exécute autant de fois.

N'oubliez jamais que votre code est en train d'être compilé dans un ensemble de commandes que le CPU va exécuter (pointeurs de mémoire, pointeurs de niveau de code d'instruction, interruptions, etc.). C’est ainsi que les ordinateurs fonctionnent et sont plus faciles à comprendre au niveau du microcontrôleur, comme un processeur ARM ou Motorola, mais il en va de même pour les machines sophistiquées que nous utilisons actuellement.

Votre code ne fonctionne tout simplement pas comme vous l'écrivez (ça sonne fou, non?). Il est exécuté comme il est compilé pour être exécuté en tant qu’instruction au niveau machine (écrire un compilateur n’est pas amusant). L'expression mathématique et la logique peuvent être compilées dans un tas d'assemblages, de code de niveau machine et cela dépend de la façon dont le compilateur choisit de l'interpréter (le décalage de bits, etc., vous souvenez-vous des mathématiques binaires?)

Référence: https://software.intel.com/en-us/articles/introduction-to-x64-Assembly

Il est difficile de répondre à votre question, mais comme @Bergi l'a déclaré, plus il y a d'opérations, plus longtemps, mais pourquoi? Plus il faut de cycles d'horloge pour exécuter votre code. Dual core, quad core, threading, Assembly (langage machine), c’est complexe. Mais aucun code n'est exécuté comme vous l'avez écrit. C++, C, Pascal, JavaScript, Java, sauf si vous écrivez dans Assembly (même compilé en code machine), mais il est plus proche du code d'exécution réel.

Une maîtrise en CS et vous aurez à compter les cycles d'horloge et les temps de tri. Vous aurez probablement votre propre langage encadré sur des jeux d'instructions de la machine.

La plupart des gens disent qui s'en soucie? La mémoire n’est pas chère aujourd’hui et les processeurs sont de plus en plus rapides.

Mais il existe certaines applications critiques où 10 ms sont importantes, où une interruption immédiate est nécessaire, etc. 

Commerce, NASA, centrale nucléaire, entrepreneurs de défense, robotique, vous voyez l'idée. . . 

Je vote, laissez-le rouler et continuez d'avancer.

Salutations, Wookie

2
Wookies-Will-Code

Étant donné que l'élément que vous recherchez se trouve toujours approximativement au milieu du tableau, vous devez vous attendre à ce que la version qui entre au début et à la fin du tableau prenne environ deux fois plus longtemps que celle qui commence au début.

Chaque mise à jour variable prend du temps, chaque comparaison prend du temps et vous en effectuez deux fois plus. Puisque vous savez qu'il faudra une ou deux fois moins d'itérations de la boucle pour se terminer dans cette version, vous devriez en déduire qu'il vous en coûtera environ deux fois plus de temps processeur.

Cette stratégie a toujours la même complexité de temps O(n) puisqu'elle ne regarde chaque élément qu'une seule fois. C'est particulièrement pire lorsque cet élément se trouve près du centre de la liste. Si l'approche est proche de la fin, cette approche aura une meilleure durée d'exécution. Essayez de rechercher l'élément 90 dans les deux, par exemple.

1
Justin Summerlin

La réponse choisie est excellente. J'aimerais ajouter un autre aspect: essayez findIndex(), c'est 2 à 3 fois plus rapide que d'utiliser des boucles:

const arr = Array.from({length: 900}, (_, i) => i);
const n = 51;
const len = arr.length;

console.time("iterate from start");
for (let i = 0; i < len; i++) {
  if (arr[i] === n) break;
}
console.timeEnd("iterate from start");

console.time("iterate using findIndex");
var i = arr.findIndex(function(v) {
  return v === n;
});
console.timeEnd("iterate using findIndex");

1
oriadam

Les autres réponses ici couvrent les raisons principales, mais je pense qu'un ajout intéressant pourrait être la mention cache. 

En général, l'accès séquentiel à un tableau sera plus efficace, en particulier avec les grands tableaux. Lorsque votre CPU lit un tableau dans la mémoire, il récupère également les emplacements de mémoire à proximité dans le cache. Cela signifie que lorsque vous récupérez l'élément n, l'élément n+1 est également probablement chargé dans le cache. Maintenant, le cache est relativement grand ces jours-ci, votre baie 100 int peut probablement s’intégrer confortablement dans le cache. Cependant, sur un tableau de taille beaucoup plus grande, la lecture séquentielle sera plus rapide que de basculer entre le début et la fin du tableau.

0
Adrian Smith