IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Unity se prépare à remplacer le C++ par C#,
En éliminant une série d'éléments qui nuisent à la performance

Le , par dourouc05

504PARTAGES

16  3 
Unity est un moteur de jeu extrêmement utilisé actuellement, notamment pour ses outils d'édition complets et conviviaux. Cependant, le moteur doit suivre l'évolution des machines : depuis une dizaine d'années, les processeurs ne montent plus en fréquence, mais plutôt en nombre de cœurs. En d'autres termes, pour exploiter la nouvelle performance disponible, les jeux doivent exécuter leur code sur différents cœurs, à travers différents fils d'exécution. Pourtant, depuis le temps que la technologie est disponible, peu de jeux y arrivent vraiment. De fait, les problèmes pour l'écriture de tel code sont nombreux : il faut s'assurer que deux fils ne tentent pas d'écrire en même temps dans la même variable, par exemple. Ceci implique que l'un des deux fils doit alors attendre l'autre : si le code est légèrement mal écrit, il n'est pas impossible qu'ils s'attendent mutuellement à l'infini (une situation nommée étreinte fatale).

Pour éviter ces inconvénients, il est possible de suivre quelques séries de règles. Néanmoins, les développeurs ont peu d'outils pour s'assurer qu'elles sont suivies : un code qui ne les respecte pas continuera de compiler, pourrait fonctionner nonante-neuf fois sur cent. C'est une des raisons pour lesquelles Unity travaille sur un nouveau compilateur C#, dénommé Burst : le non-respect de ces règles provoquera une erreur de compilation.

Pour y arriver, le code doit être écrit comme une collection de tâches à effectuer. Chacune de ces tâches effectue quelques transformations sur des données, mais n'a aucun effet de bord (en suivant les préceptes de la programmation fonctionnelle) indésirable. Le programmeur doit spécifier les zones de mémoire auxquelles il peut accéder en lecture seule et celles où il souhaite lire et écrire des données : le compilateur s'assurera qu'il n'utilise rien en dehors de ces déclarations. Elles permettent de gérer une très grande partie des besoins en calcul parallèle. Ensuite, un ordonnanceur détermine la meilleure manière d'exécuter ces tâches, en temps réel, grâce à ces informations supplémentaires : il peut s'assurer qu'aucune tâche ne viendra écrire des données là où une autre tente de lire ou d'écrire, par exemple. Ce mécanisme augmente fortement la sécurité du code écrit, bon nombre de défauts sont remarqués peu après l'écriture du code ; il est aussi impossible de créer une course de données ou une étreinte fatale, les résultats sont entièrement déterministes, peu importe le nombre de fils d'exécution utilisés pour gérer les tâches ou le nombre d'interruptions d'une tâche.

Burst n'a pas que cet objectif de faciliter la programmation parallèle : il est aussi utilisé dans les parties les plus critiques (d'un point de vue performance) du code de Unity. Jusqu'à présent, ces endroits étaient écrits en C++, mais les compilateurs actuels ne sont pas entièrement satisfaisants. En effet, si un développeur souhaite qu'une boucle soit vectorisée, il n'a aucune garantie que le compilateur le fera, à cause de changements pourtant a priori sans impact (pour une addition entre deux vecteurs, par exemple, le compilateur doit prouver formellement que, dans tous les cas possibles et imaginables, les deux vecteurs ne correspondent pas aux mêmes adresses en mémoire). Et encore, il faut que tous les compilateurs utilisés pour Unity sur les différentes plateformes visées effectuent correctement cette vectorisation — sans oublier qu'une mise à jour du compilateur peut aussi être à l'origine d'une baisse de performance.

Pourquoi Burst et pas un compilateur existant ? La performance est un point critique : si un boucle n'est pas vectorisée, ce n'est pas simplement dommage (ce que la plupart des compilateurs se disent), c'est un vrai problème qui doit être corrigé rapidement. De plus, le binaire généré doit être sûr : les erreurs de dépassement de tampon et de déréférencements hasardeux doivent être découvertes au plus tôt, avec de vrais messages d'erreur plutôt que des comportements indéfinis (à l'origine de nombreux problèmes de sécurité). Finalement, il doit gérer toutes les architectures sur lesquelles Unity existe : changer de langage parce qu'on développe un jeu pour console, PC ou mobile n'a pas de sens. Ce compilateur devrait effectivement être utilisé tant pour le moteur que les jeux.

Ces besoins posés, il faut encore choisir le langage d'entrée de ce compilateur : une variante ou un sous-ensemble du C, du C++, de C# ou encore un nouveau langage ? Le nouveau langage semble à bannir, pour éviter de devoir former des gens à ce nouvel outil ; C# a la préférence du point de vue des utilisateurs, puisqu'il est déjà utilisé par eux : le moteur de jeu serait alors codé dans le même langage que les jeux eux-mêmes. De plus, C# dispose déjà d'un très grand écosystème (des EDI, des compilateurs ouverts). Au contraire, C++ souffre toujours de son héritage du C, avec des inclusions pas toujours évidentes à déterminer et des temps de compilation énormes — des défauts que C++20 vient corriger en partie —, malgré son obsession sur la performance (une chose que C# n'a pas).

La décision a été prise de partir sur C#, mais en éliminant une série d'éléments qui nuisent à la performance : la bibliothèque standard, en bonne partie, la réflexion, le ramasse-miettes et les allocations (ce qui revient à interdire l'utilisation de classes, seules les structures restent autorisées), les appels virtuels. Autant dire qu'on se retrouve, à certains points de vue, aussi bien outillés qu'en C (avec les possibilités d'oubli de désallouer la mémoire qui n'est plus nécessaire) — mais ce sous-ensemble du langage n'est vraiment adapté qu'aux parties vraiment importantes d'un point de vue performance, pas à la globalité du moteur. Ce sous-ensemble est nommé High-Performance C# (ou encore HPC#).

Burst ne fonctionne pas vraiment comme un compilateur complet : il ne prend pas en entrée une énorme quantité de code, mais seulement le point d'entrée vers une boucle cruciale. Il se limite à la compiler comme une fonction, ainsi que tout ce qu'elle appelle (puisque les appels virtuels sont interdits, les fonctions appelées sont faciles à déterminer). Le niveau d'optimisation est extrêmement élevé : puisque Burst se focalise sur certaines portions de code, il peut y passer du temps. Notamment, il n'existe presque plus un seul appel de fonction en sortie : importer tout le code permet bien souvent d'éliminer une série de vérifications d'usage en début de fonction. Puisque le seul type de tableau possible est NativeArray et que ces tableaux ne permettent pas de faire des références à d'autres tableaux, deux NativeArray seront toujours distincts en mémoire : la vectorisation peut toujours se faire. Dans le futur, Burst pourra utiliser le même niveau de connaissance sur les fonctions mathématiques utilisées : le sinus d'un angle est presque égal à cet angle s'il est très petit, c'est-à-dire qu'on peut alors remplacer sin(x) par x sans grande perte de précision (ou par un développement en série de Taylor d'un plus grand ordre, si l'angle est un peu plus grand).

La première itération de Burst, avec HPC# et le système de tâches, est arrivée avec Unity 2018.1. Le code généré est parfois plus rapide que la version précédente en C++, parfois plus lente — mais les développeurs sont confiants qu'ils arriveront toujours à au moins atteindre le même niveau de performance que C++. Un élément crucial doit aussi être pris en compte : combien de temps et d'énergie a-t-il fallu pour atteindre ce niveau de performance ? Le code qui détermine les faces visibles d'un objet (culling) a été réécrit avec HPC# : alors que le code C++ était très complexe pour s'assurer qu'il soit toujours vectorisé (sans écrire d'assembleur spécifique à une plateforme), le code HPC# est quatre fois plus court… pour la même performance. Tout le reste du moteur fera la transition vers HPC#, un jour ou l'autre, tant que le bout de code concerné est critique d'un point de vue performance : le code HPC# est souvent plus facile à optimiser, le langage rend plus difficile l'écriture de code faux.

Source : On DOTS: C++ & C#.

Et vous ?

Qu'en pensez-vous ?

Une erreur dans cette actualité ? Signalez-nous-la !

Avatar de Bousk
Rédacteur/Modérateur https://www.developpez.com
Le 27/02/2019 à 14:30
On a tous déjà entendu en réunion "J'ai corrigé un problème de course de données et une étreinte fatale"
C'est là que la traduction française à tout va devient ridicule imo.

Pour le reste, je suis surpris et un peu dubitatif.
Déjà, il va bien falloir le créer ce nouveau compilateur, et il devra être spécifique à chaque plateforme.
Puis écrire du C#, en supprimant tellement de trucs qu'on se retrouve avec du C, au final ça revient pas à dire qu'ils utilisent ça qu'en tant que langage de script simplifié ? Le compilateur serait donc plus un parser qu'un compilateur réellement.
Réécrire l'engin avec ce nouveau langage est une tâche énorme, mais à terme ça colle avec les rumeurs/infos de leur souhait de passer le moteur open source, quand il sera complètement porté en C#.
11  0 
Avatar de dancingmad
Membre averti https://www.developpez.com
Le 28/02/2019 à 15:26
Pour préciser un peu, il y a une chose que C++ fait très bien (et c'est l'un des seuls à le faire correctement), c'est la gestion des allocations et du layout mémoire. En C++ en peut contrôler de manière très précise l'alignement mémoire, la taille des structures utilisées et surtout à quel moment une allocation/déallocation a lieu, cet c'est extrêmement critique pour un jeu vidéo dans lequel une trame fait au plus 33ms.

Dans la plupart des langages modernes type C#, la mémoire est gérées par le runtime et c'est un très gros problème pour plusieurs raisons:

1) C'est impossible de définir de manière sûre a quel endroit une struct va être alloué alors qu'on a plutôt tendance à vouloir utiliser différentes stratégies d'allocation selon le type d'objets avec des pools etc. C'est surtout par principe de localité: si on veut itérer sur 10000 objets du même type, on a envie que ces objets soient alloués côté à côté pour éviter les cache miss.

2) La philosophie globale de ces langage fait que la bibliothèque standard et la plupart des bibliothèques ne se posent pas trop de question sur les allocations puisque de toute façon tout est géré par le système. Or chaque allocation est assez coûteuse et encore une fois on ne peut pas dépasser 33ms par trame. Utiliser une expression lambda en C# déclenche des allocations (même quand on ne capture aucune variable), faire une requête LINQ déclenche des allocations, parfois même utiliser des algorithme de tri déclenche des allocations, etc. En tant que dev de jeu-vidéo on veut éviter ça absolument pour des questions de perfs.

3) Au sujet du layout mémoire, on a besoin dans le JV de contrôler exactement le contenu de certains buffers parce qu'on va les envoyer à la carte graphique par exemple, ou encore parce qu'on sait qu'on a des ressources limitées sur consoles. Oui malgré la puissance des consoles actuelles on a encore des gros problèmes de mémoire sur les gros jeux AAA parce qu'on pousse toujours la machine dans ses derniers retranchements. En C# c'est très difficile et très verbeux de contrôler la mémoire, sans compter que l'allocateur utilise des "astuces" qui rendent un enfer la vie des progs. Exemple concret: si on alloue des objets en pool en C#, il arrive qu'il y ait des "trous" - ce qui normal pour garantir l'alignement - et ces trous sont parfois utilisé par l'allocateur pour allouer des petits objets de 1 ou 2 octets. On se retrouve avec une structure qui contient une autre structure et donc toute tentative de faire un memset(0) ou toute autre operation de ce type pourrait corrompre la mémoire...

4) Mon dernier point et non le moins important, le garbage collector. Il peut se déclencher à n'importe quel moment, stoppe absolument tous les theads (même les threads critiques comme le son) et peut prendre facilement 100ms pour de grosses applications... Autrement dit 3 trames. Rien que pour cet argument, non on ne peut pas utiliser C# pour du gros jeu.

Pour en revenir au HPC et Burst, le principe est justement d'utiliser un sous-ensemble de C# qui permet d'éviter la plupart des problèmes cités avant à moindre frais: on n'utilise que des struct et pas des class pour ne pas avoir d'overhead mémoire ni provoquer des allocs intempestives, etc. On aura toujours des problèmes de garbage collector mais qui seront moins prononcés vu qu'on aura moins d'objets alloués sur le tas. Bon en plus Unity n'est pas un moteur AAA donc c'est largement suffisant pour la plupart des utilisations.
9  0 
Avatar de ParseCoder
Membre averti https://www.developpez.com
Le 27/02/2019 à 19:34
Je trouve le titre assez trompeur car C# n'a pas remplacé C++. C'est un petit sous ensemble de C#, tout petit.
8  0 
Avatar de Aurelien.Regat-Barrel
Expert éminent sénior https://www.developpez.com
Le 28/02/2019 à 16:14
Citation Envoyé par Matthieu76 Voir le message
Tous ça me donne envie de réécrire mes project C++ en C#. Le C++ commence à devenir un language dépassé, non ? Quel intérêt de commencer un projet en C++ quand on a la possibilité de le faire en C# ?
La news est assez trompeuse. Il serait plus juste de dire qu'ils ont créé leur propre langage dont la syntaxe correspond à un sous ensemble de C# et qui est très spécialisé pour leur besoin très précis (ils ne s'agit pas recoder tout Unity avec ce langage). Sur leur blog ils donnent le liens vers quelqu'un qui a fait des tests assez poussés de perf C# "normal" vs C++, et je pense que ça résume pourquoi on utilise C++ malgré sa complexité:

So the summary of C# performance so far:

- Basic .NET Core performance is roughly 2x slower than C++ performance, on both Windows & Mac.
- For simple types like “vector” or “color”, you really want to use struct to avoid allocation & garbage collection overhead. Otherwise your code will run 30 (!) times slower on a 16-thread PC, and 3 times slower on a 8-thread Mac. I don’t know why such a performance discrepancy.
- Mono .NET implementation is about 3x slower than .NET Core implementation, at least on Mac, which makes it ~6x slower than C++. I suspect the Mono JIT is much “weaker” than RyuJIT and does way less inlining etc.; you might want to “manually inline” your heavily used functions.
7  0 
Avatar de Kannagi
Expert éminent sénior https://www.developpez.com
Le 28/02/2019 à 11:06
Si tu te pose la question :
1)Pourquoi le C++ est dépassé par rapport au C# , dans quoi au juste ?
Surtout que en terme de paradigme le C++ dépasse probablement tout les autres langages vu qu'il permet de coder de façon très différente.
2)Dans le Jeux vidéo (entre autre) on a besoin de faire du bas niveau et quel chance le C++ peut être bas niveau ,par exemple on l'utilise dans Arduino pour programmer dans l'embarquée et donc l'avantage du C++ c'est qu'il peut communiquer aisément avec le Hardware (ce n'est pas rien).
3)Ils existent moult compilateur pour le C++ , qui est de plus très efficace donc le C++ est non seulement plus performant , mais il vise plus de plateforme.
Quid des plateformes exotiques ?
Rare sont les SDK sur consoles qui offre autre chose que du C/C++ , et refaire un compilateur qui vise autant de plateforme que Unity est loin d’être aisées ...
7  1 
Avatar de Aurelien.Regat-Barrel
Expert éminent sénior https://www.developpez.com
Le 28/02/2019 à 22:57
Citation Envoyé par Matthieu76 Voir le message
On dit parfois que C++ ne donne pas de la performance, il donne accès à la performance. Les techniques d'optimisations sont les mêmes au delà du langage car c'est le hardware sous jacent qui dicte ça, et c'est une expertise à part entière. La différence en C++ c'est qu'il est plus "hardware sympathic" et en ne te faisant pas payer pour ce que tu n'utilises pas. Au hasard le garbage collector qui met régulièrement en micro-pause ton programme et déclenche des recopies complète de tes data en mémoire.

Pour ce qui est des micro benchmark, ile ne veulent pas dire grand chose car la performance dans und vraie application se trouve aussi (voire surtout) dans la data, et plus exactement dans l'empreinte et l'agencement mémoire de ton programme. Dès que tu satures la taille de tes caches (quelques Mo), les performances s'écroulent (du genre 10 à 50 fois plus lent). Et si tes data ne sont pas ordonnées de manière continue, c'est la même sanction. Et donc un langage qui t'imposent d'allouer à tout va sur le heap en rajoutant en plus un overhead pour le type System.Object de base obligatoire c'est catastrophique pour les perfs passé un certaine taille.

Une intro de qualité par un exellent speaker (vers la 22eme min):
https://channel9.msdn.com/Events/Build/2014/2-661

Concernant les erreurs de programmation qui dégradent les perf, la règle d'or en optimisation c'est de commencer par profiler ton code, ainsi tu découvres vite les erreurs de ce type. Mais aussi de nos jours on a des outils d'analyse statique de code (gratuits) qui peuvent détecter certaines copies inutiles. CLion par exemple te signale dans ton code ce genre de cas et t'invite à faire un move ou un const ref.

Enfin, la tendance moderne est de prendre en input un string_view ou un span quand tu ne fait que consulter la data, de cette manière il n'y a aucun risque en terme de perfs en plus d'avoir une meilleur sémantique vis à vis du code.

Ces exemples résument mes propos initiaux : oui en C++ il est facile (comme dans d'autres langages) d'écrire du code avec de mauvaises performances. Mais une fois localisé, on a beaucoup d'options assez simples pour régler efficacement le problème.
6  0 
Avatar de
https://www.developpez.com
Le 28/02/2019 à 22:35
Citation Envoyé par Mingolito Voir le message
"Nonante" c'est mieux que "quatre vingt dix", plus logique et plus court.
C'est surtout que ça n'a rien à voir avec la news. Si vous voulez disserter là dessus, pourquoi ne pas créer un topic dans la section taverne ?
5  0 
Avatar de mattdef
Membre averti https://www.developpez.com
Le 28/02/2019 à 10:06
Citation Envoyé par Steinvikel Voir le message
C'est vrai qu'en français dès lors qu'on dépasse "treize" il devient plus léger d'utiliser des chiffres et non des mots. Mais question rédaction typographique, on fait alors quelques entorses aux coutumes, qui pousserait plutôt à tout écrire en toutes lettres. ^^'
Au delà de 99, c'est le français qui est plutot agréable non ?

Mille cent un <> one thousand one hundred and one
4  0 
Avatar de codec_abc
Membre confirmé https://www.developpez.com
Le 28/02/2019 à 13:54
Citation Envoyé par Kannagi Voir le message
Si tu te pose la question :
1)Pourquoi le C++ est dépassé par rapport au C# , dans quoi au juste ?
Surtout que en terme de paradigme le C++ dépasse probablement tout les autres langages vu qu'il permet de coder de façon très différente.
2)Dans le Jeux vidéo (entre autre) on a besoin de faire du bas niveau et quel chance le C++ peut être bas niveau ,par exemple on l'utilise dans Arduino pour programmer dans l'embarquée et donc l'avantage du C++ c'est qu'il peut communiquer aisément avec le Hardware (ce n'est pas rien).
3)Ils existent moult compilateur pour le C++ , qui est de plus très efficace donc le C++ est non seulement plus performant , mais il vise plus de plateforme.
Quid des plateformes exotiques ?
Rare sont les SDK sur consoles qui offre autre chose que du C/C++ , et refaire un compilateur qui vise autant de plateforme que Unity est loin d’être aisées ...
Pour le point 2, je ne comprends pas ou tu veux en venir car tu parles de jeux vidéo et d'arduino dans la même phrase alors que les domaines n'ont rien à voir. Dans un domaine on est sur des machines très puissantes et dans l'autre sur des machines très lentes sans mémoire dynamique. Je ne vois pas de point commun entre les 2 domaines. De plus, tu veux dire quoi par bas niveau ? Utiliser un allocateur spécifique, utiliser des instructions SIMDs/NEON, contrôler l'alignement de structures de données ? Car une bonne partie de cela est faisable en C# pour peu que l'on se mette quelque contraintes (ce qui est le cas avec le compilateur Burst).

Pour le point 3, Ça dépend du compilateur que tu utilises. En l’occurrence Burst, le compilo que développe Unity utilise LLVM pour le back-end. Donc en soit, ce n'est pas une tache démesuré que de faire un front-end à LLVM. Plein de langages le font déjà (Rust, Swift, CUDA et plein d'autres le font). Et je pense (à vérifier) que le fait d'utiliser LLVM permet de générer du code qui fonctionne sur les principales plateformes de jeux-vidéo (consoles, PCs, mobiles).
3  0 
Avatar de Pyramidev
Expert éminent https://www.developpez.com
Le 01/03/2019 à 13:07
Citation Envoyé par Matthieu76  Voir le message
Avoir du code avec des objets contenant des vectors d'objet contenant des vectors d'objet contenant des vectors de float et faire des calculs en boucle sur ces float ça doit être au moins 100 fois plus lent que si j'avais juste un tableau 3D de float. Après si j'avais juste des structs et des tableaux de float j'aurais un code plus rapide mais complètement imbitable avec des erreurs de calcul et d'index partout, un enfer à debugger !

Tu n'es pas obligé de créer un vecteur de vecteur de vecteur pour avoir une syntaxe commode. Tu peux aussi encapsuler ton tableau 3D dans une classe dont tu choisis la syntaxe.

Exemple simple de template de classe pour gérer un tableau 3D dont on ne connaît les dimensions qu'au runtime :
Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <cassert> 
#include <vector> 
  
template<class T> 
class DynamicArray3D { 
private: 
	size_t m_sizeX; 
	size_t m_sizeY; 
	size_t m_sizeZ; 
	std::vector<T> m_elements; 
public: 
	DynamicArray3D(size_t sizeX, size_t sizeY, size_t sizeZ) : 
		m_sizeX{sizeX}, 
		m_sizeY{sizeY}, 
		m_sizeZ{sizeZ}, 
		m_elements(sizeX*sizeY*sizeZ) 
	{} 
	T& operator()(size_t x, size_t y, size_t z) { 
		assert(x < m_sizeX); 
		assert(y < m_sizeY); 
		assert(z < m_sizeZ); 
		return m_elements[x + y*m_sizeX + z*m_sizeX*m_sizeY]; 
	} 
	T const& operator()(size_t x, size_t y, size_t z) const { 
		assert(x < m_sizeX); 
		assert(y < m_sizeY); 
		assert(z < m_sizeZ); 
		return m_elements[x + y*m_sizeX + z*m_sizeX*m_sizeY]; 
	} 
	size_t getSizeX() const { return m_sizeX; } 
	size_t getSizeY() const { return m_sizeY; } 
	size_t getSizeZ() const { return m_sizeZ; } 
};

Exemple d'utilisation :
Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream> 
  
DynamicArray3D<double> createMyDynamicArray3D(size_t sizeX, size_t sizeY, size_t sizeZ) 
{ 
	DynamicArray3D<double> dynArr{sizeX, sizeY, sizeZ}; 
	for(size_t z = 0; z < sizeZ; ++z) 
		for(size_t y = 0; y < sizeY; ++y) 
			for(size_t x = 0; x < sizeX; ++x) 
				dynArr(x, y, z) = 100*z + 10*y + x; 
	return dynArr; 
} 
  
void print(DynamicArray3D<double> const& dynArr) 
{ 
	const size_t sizeX = dynArr.getSizeX(); 
	const size_t sizeY = dynArr.getSizeY(); 
	const size_t sizeZ = dynArr.getSizeZ(); 
	for(size_t z = 0; z < sizeZ; ++z) { 
		for(size_t y = 0; y < sizeY; ++y) { 
			for(size_t x = 0; x < sizeX; ++x) { 
				std::cout << dynArr(x, y, z); 
				if(x+1 < sizeX) 
					std::cout << ", "; 
			} 
			if(y+1 < sizeY) 
				std::cout << '\n'; 
		} 
		if(z+1 < sizeZ) 
			std::cout << "\n\n"; 
	} 
} 
  
int main() 
{ 
	constexpr size_t sizeX = 5; 
	constexpr size_t sizeY = 3; 
	constexpr size_t sizeZ = 2; 
	const DynamicArray3D<double> dynArr = createMyDynamicArray3D(sizeX, sizeY, sizeZ); 
	print(dynArr); 
	return 0; 
}
3  0