Pendant que je réfléchissais à l'utilisation de la mémoire de différents types, j'ai commencé à devenir un peu confus de la façon dont Java utilise la mémoire pour les entiers lorsqu'elle est passée à une méthode.
Dis, j'avais le code suivant:
public static void main (String[] args){
int i = 4;
addUp(i);
}
public static int addUp(int i){
if(i == 0) return 0;
else return addUp(i - 1);
}
Dans cet exemple suivant, je me demande si ma logique suivante était correcte:
Cependant, si je devais toujours le passer à travers un tableau:
public static void main (String[] args){
int[] i = {4};
// int tempI = i[0];
addUp(i);
}
public static int addUp(int[] i){
if(i[0] == 0) return 0;
else return addUp(i[0] = i[0] - 1);
}
- Puisque je crée un tableau d'entiers de taille 1 et que je passe ensuite à addUp qui sera à nouveau transmis pour addUp (i [0] == 3), addUp (i [0] == 2), addUp (i [0] == 1), addUp (i [0] == 0), je n'ai eu qu'à utiliser 1 espace mémoire de tableau entier et donc beaucoup plus rentable. De plus, si je devais faire une valeur int au préalable pour stocker la valeur initiale de i [0], j'ai toujours ma valeur "d'origine".
Ensuite, cela m'amène à la question: pourquoi les gens passent-ils des primitives comme int dans Java? N'est-il pas beaucoup plus efficace en mémoire de simplement passer les valeurs de tableau de ces primitives? Ou est-ce le premier exemple en quelque sorte toujours juste O(1) mémoire?
Et en plus de cette question, je me demande juste les différences de mémoire entre int [] et int surtout pour une taille de 1. Merci d'avance. Je me demandais simplement d'être plus efficace en mémoire avec Java et cela m'est venu à l'esprit.
Merci pour toutes les réponses! Je me demande maintenant si je devais "analyser" la mémoire big-oh de chaque code, seraient-ils tous les deux considérés O(1) ou est-ce mal de supposer?
Ce qui vous manque ici: les valeurs int dans votre exemple vont sur stack, pas sur le tas.
Et c'est beaucoup moins de surcharge pour gérer les valeurs primitives de taille fixe existant sur la pile - par rapport aux objets sur le tas!
En d'autres termes: l'utilisation d'un "pointeur" signifie que vous devez créer un nouvel objet sur le tas. Tous les objets vivent sur le tas; il y a non pile pour les tableaux! Et les objets sont soumis à garbage collection immédiatement après que vous avez cessé de les utiliser. D'un autre côté, les piles vont et viennent lorsque vous invoquez des méthodes!
Au-delà de cela: gardez à l'esprit que les abstractions que les langages de programmation nous fournissent sont créées pour nous aider à écrire du code facile à lire, à comprendre et à maintenir. Votre approche consiste essentiellement à effectuer une sorte de réglage fin qui conduit à un code plus compliqué. Et ce n'est pas ainsi que Java résout ces problèmes.
Signification: avec Java, la véritable "magie des performances" se produit au moment de l'exécution, lorsque le compilateur juste à temps entre en action! Vous voyez, le JIT peut les appels en ligne aux petites méthodes lorsque la méthode est invoquée "assez souvent". Et puis il devient encore plus important de garder les données "proches" ensemble. Comme dans: lorsque les données vivent sur le tas, vous devrez peut-être accéder à memory pour obtenir une valeur. Alors que les éléments vivant sur la pile - peuvent encore être "fermés" (comme dans: dans le cache du processeur). Votre petite idée d'optimiser l'utilisation de la mémoire pourrait donc ralentir l'exécution du programme par ordre de grandeur. Parce qu'aujourd'hui encore, il existe des ordres de grandeur entre l'accès au cache du processeur et la lecture de la mémoire principale.
Pour faire court: évitez de vous lancer dans un tel "micro-réglage" pour les performances ou l'utilisation de la mémoire: la JVM est optimisée pour les cas d'utilisation "normaux". Vos tentatives d'introduire des solutions intelligentes peuvent donc facilement aboutir à des résultats "moins bons".
Donc - lorsque vous vous souciez des performances: faites ce que tout le monde fait. Et si vous un vraiment attention - alors apprenez comment la JVM fonctionne. Il s'avère que même mes connaissances sont légèrement dépassées - car les commentaires impliquent qu'un JIT peut en ligne objets sur la pile. En ce sens: concentrez-vous sur l'écriture d'un code propre et élégant qui résout le problème de manière simple!
Enfin: cela peut changer à un moment donné. Il existe des idées pour introduire true value value objets à Java. Qui vivent essentiellement sur la pile, pas sur le tas. Mais ne vous attendez pas à ce que cela se produise avant Java10. Ou 11. Ou ... (Je pense que ceci serait pertinent ici).
Plusieurs choses:
La première chose sera de séparer les cheveux, mais lorsque vous passez un int dans Java vous allouez 4 octets sur la pile, et lorsque vous passez un tableau (car c'est une référence), vous allouez réellement 8 octets (en supposant une architecture x64) sur la pile, plus les 4 octets supplémentaires qui stockent l'int dans le tas.
Plus important encore, les données qui vivent dans le tableau sont allouées dans le tas, tandis que la référence au tableau lui-même est allouée à la pile, lors du passage d'un entier, aucune allocation de tas n'est requise, la primitive est uniquement allouée dans la pile. Au fil du temps, la réduction des allocations de tas signifie que le garbage collector aura moins de choses à nettoyer. Alors que le nettoyage des stack-frames est trivial et ne nécessite pas de traitement supplémentaire.
Cependant, tout cela est théorique (à mon humble avis) car dans la pratique, lorsque vous avez des collections complexes de variables et d'objets, vous allez probablement finir par les regrouper dans une classe. En général, vous devez écrire pour promouvoir la lisibilité et la maintenabilité plutôt que d'essayer de retirer chaque dernière baisse de performances de la JVM. La JVM est assez rapide, et il y a toujours loi de Moore comme filet de sécurité.
Il serait difficile d'analyser le Big-O pour chacun, car pour obtenir une image réelle, vous devez prendre en compte le comportement du garbage collector et ce comportement dépend fortement de la JVM elle-même et de tout runtime (JIT) optimisations que la JVM a apportées à votre code.
N'oubliez pas Donald Knuth les sages paroles que "l'optimisation prématurée est la racine de tout mal"
Écrivez du code qui évite le micro-réglage, le code qui favorise la lisibilité et la maintenabilité se comportera mieux à long terme.
Si votre hypothèse est que les arguments passés aux fonctions consomment nécessairement de la mémoire (ce qui est faux en passant), alors dans votre deuxième exemple qui passe un tableau, une copie de la référence au tableau est faite. Cette référence peut en fait être plus grande qu'un int, il est peu probable qu'elle soit plus petite.
Que ces méthodes prennent O(1) ou O(N) dépend du compilateur. (Ici N est la valeur de i
ou i[0]
, selon.) Si le compilateur utilise l'optimisation de récursivité de queue, l'espace de pile pour les paramètres, les variables locales et l'adresse de retour peut être réutilisé et l'implémentation sera alors O(1) En l'absence d'optimisation de récursivité de la queue, la complexité de l'espace est la même que la complexité temporelle, O (N).
Fondamentalement, l'optimisation de la récursivité de queue revient (dans ce cas) au compilateur à réécrire votre code sous la forme
public static int addUp(int i){
while(i != 0) i = i-1 ;
return 0;
}
ou
public static int addUp(int[] i){
while(i[0] != 0) i[0] = i[0] - 1 ;
return 0 ;
}
Un bon optimiseur pourrait optimiser davantage les boucles.
Pour autant que je sache, aucun Java n'implémentent actuellement l'optimisation de la récursivité de queue, mais il n'y a aucune raison technique pour laquelle cela ne peut pas être fait dans de nombreux cas.
En fait, lorsque vous passez un tableau comme paramètre à une méthode - une référence à ce tableau est passée sous le capot. Le tableau lui-même est stocké sur le tas. Et la référence peut être de taille 4 ou 8 octets (selon l'architecture du processeur, la mise en œuvre de la JVM, etc.; encore plus, JLS ne dit rien sur la taille d'un référence est en mémoire).
En revanche, la valeur primitive int
ne consomme toujours que 4 octets et réside sur la pile.
Lorsque vous passez un tableau, le contenu du tableau peut être modifié par la méthode qui reçoit le tableau. Lorsque vous passez des primitives int, ces primitives ne peuvent pas être modifiées par la méthode qui les reçoit. C'est pourquoi parfois vous pouvez utiliser des primitives et parfois des tableaux.
En général également, dans la programmation Java Java, vous avez tendance à favoriser la lisibilité et à laisser ce type d'optimisations de mémoire être fait par le compilateur JIT.
La référence de tableau int prend en fait plus d'espace dans les trames de pile qu'une primitive int (8 octets contre 4). Vous utilisez en fait plus d'espace.
Mais je pense que la principale raison pour laquelle les gens préfèrent la première façon est parce qu'elle est plus claire et plus lisible.
En fait, les gens font les choses beaucoup plus près de la seconde lorsque plus d'ts sont impliqués.