Changeset 97ac01d3 for doc/theses/mike_brooks_MMath
- Timestamp:
- Nov 5, 2024, 7:27:44 PM (10 days ago)
- Branches:
- master
- Children:
- 5ba756c
- Parents:
- 7d95bce9
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
doc/theses/mike_brooks_MMath/array.tex
r7d95bce9 r97ac01d3 1092 1092 1093 1093 1094 1094 \section{Array lifecycle} 1095 1096 An array is an aggregate, like a struct. 1097 Both wrap subordinate objects. 1098 An arbitrary other type, like @string@, can be used as a struct member or an array element. 1099 When it is, that type's lifecycle assumptions must be respected. 1100 In the case of @string@, it would be disasterous to start calling string-management functions on uninitialized storage. 1101 The @string@ type provides its constructors and destructor; 1102 \CFA is supposed to provide a reasonable assurance that they will be called, even if the string is wrapped in something else, even if the user-programmer isn't thinking about it. 1103 1104 Preexisting \CFA mechanisms achieve this requirement in correctness, but not with reasonable performance. 1105 Furthermore, advanced users of arrays need an exception to the bluntly-stated requirement, which does not come up when working with other aggregates. 1106 So the \CFA array has subtleties in its support for element lifecycle. 1107 1108 The preexisting \CFA support for contained-element lifecycle is based on a recursive implementation of the object-type (otype) pseudo-trait. 1109 A type is an otype if it provides a parameterless constructor, copy constructor, assignment operator and destructor. 1110 When declaring a struct whose members are all otypes, the compiler generates implementations of the four otype functions for the outer struct. 1111 The generated parameterless constructor for the outer struct calls the parameterless constructor for each memeber, and the other otype functions work similarly. 1112 For a member that is a C array, these calls occur in loops, which work correctly for dynamic bounds. 1113 This logic works the same, whether the member is a concrete type (that happens to be an otype) or if the member is a polymorphic type variable asserted to be an otype (which is implicit in the default syntax, @forall(T)@). 1114 The \CFA array has the simplified form (similar to one seen before): 1115 \begin{cfa} 1116 forall( T* ) // non-otype element, no lifecycle functions 1117 // forall( T ) // otype element, lifecycle functions asserted 1118 struct array5 { 1119 T __items[ 5 ]; 1120 }; 1121 \end{cfa} 1122 Being a struct with a C-array member, using the otype-form declaration for @T@ causes @array5(float)@ to implement otype too. 1123 1124 But this otype-recursion pattern has a performance issue. 1125 A cube of @float@ is, in the running simplification: 1126 \begin{cfa} 1127 array5( array5( array5( float ) ) ) 1128 \end{cfa} 1129 Its lifecycle functions, under the otype-recursion pattern, would be: 1130 \begin{cfa} 1131 TODO: rework this crude exposition into a readable figure 1132 1133 float offers 1134 1 parameterless ctor 1135 2 copy ctor 1136 3 asgt 1137 4 dtor 1138 1139 array5(T) offers 1140 1 parameterless ctor, which asks for T to have 1141 1 parameterless ctor 1142 2 copy ctor 1143 3 asgt 1144 4 dtor 1145 2 copy ctor, which asks for T to have 1146 1 parameterless ctor 1147 2 copy ctor 1148 3 asgt 1149 4 dtor 1150 3 asgt, which asks for T to have 1151 1 parameterless ctor 1152 2 copy ctor 1153 3 asgt 1154 4 dtor 1155 4 dtor, which asks for T to have 1156 1 parameterless ctor 1157 2 copy ctor 1158 3 asgt 1159 4 dtor 1160 1161 array5(float) has 1162 1 parameterless ctor, which gets float's 1163 1 parameterless ctor 1164 2 copy ctor 1165 3 asgt 1166 4 dtor 1167 2 copy ctor, which gets float's 1168 1 parameterless ctor 1169 2 copy ctor 1170 3 asgt 1171 4 dtor 1172 3 asgt, which gets float's 1173 1 parameterless ctor 1174 2 copy ctor 1175 3 asgt 1176 4 dtor 1177 4 dtor, which gets float's 1178 1 parameterless ctor 1179 2 copy ctor 1180 3 asgt 1181 4 dtor 1182 1183 array5(array5(float)) has 1184 1 parameterless ctor, which gets array5(float)'s 1185 1 parameterless ctor, which gets float's 1186 1 parameterless ctor 1187 2 copy ctor 1188 3 asgt 1189 4 dtor 1190 2 copy ctor, which gets float's 1191 1 parameterless ctor 1192 2 copy ctor 1193 3 asgt 1194 4 dtor 1195 3 asgt, which gets float's 1196 1 parameterless ctor 1197 2 copy ctor 1198 3 asgt 1199 4 dtor 1200 4 dtor, which gets float's 1201 1 parameterless ctor 1202 2 copy ctor 1203 3 asgt 1204 4 dtor 1205 2 copy ctor, which gets array5(float)'s 1206 ... 4 children, 16 leaves 1207 3 asgt, which gets array5(float)'s 1208 ... 4 children, 16 leaves 1209 4 dtor, which gets array5(float)'s 1210 ... 4 children, 16 leaves 1211 (64 leaves) 1212 1213 array5(array5(array5(float))) has 1214 ...(256 leaves) 1215 \end{cfa} 1216 1217 Each xxx corresponds to a generated thunk. 1218 So the otype-recursion pattern generates a quantity of thunks that is exponential in the number of dimensions. 1219 Anecdotal experience with this solutuion found the resulting compile times annoying at three dimensions, and unusable at four. 1220 1221 But the otype-recursion pattern uses more assertions than are really needed. 1222 Consider how @array5(float)@'s parameterless constructor is getting all four lifecycle assertions about the element type, @float@. 1223 It only needs @float@'s parameterless constructor; it is getting a destructor (among others) and never using it. 1224 The same idea applies to the copy constructor and the destructor. 1225 For the assignment operator, it turns out that today's RAII pattern has it using a copy constructor, assignment operator and destructor. 1226 Concurrent work on the \CFA team aims to improve this whole situation. 1227 A workaround is needed for now. 1228 1229 The workaround is to provide parameterless constructor, copy constructor and destructor, defined with lean, bespoke assertions: 1230 1231 \noindent 1232 \begin{tabular}{@{}l@{\hspace{0.5in}}l@{}} 1233 \begin{cfa} 1234 // autogenerated for otype-recursion: 1235 forall( T ) 1236 void ?{}( array5(T) & this ) { 1237 for (i; 5) { ( this[i] ){}; } 1238 } 1239 forall( T ) 1240 void ?{}( array5(T) & this, array5(T) & src ) { 1241 for (i; 5) { ( this[i] ){ src[i] }; } 1242 } 1243 forall( T ) 1244 void ^?{}( array5(T) & this ) { 1245 for (i; 5) { ^( this[i] ){}; } 1246 } 1247 \end{cfa} 1248 & 1249 \begin{cfa} 1250 // lean, bespoke: 1251 forall( T* | { void @?{}( T & )@; } ) 1252 void ?{}( array5(T) & this ) { 1253 for (i; 5) { ( this[i] ){}; } 1254 } 1255 forall( T* | { void @?{}( T &, T )@; } ) 1256 void ?{}( array5(T) & this, array5(T) & src ) { 1257 for (i; 5) { ( this[i] ){ src[i] }; } 1258 } 1259 forall( T* | { void @?{}( T & )@; } ) 1260 void ^?{}( array5(T) & this ) { 1261 for (i; 5) { ^( this[i] ){}; } 1262 } 1263 \end{cfa} 1264 \end{tabular} 1265 Moreover, the assignment operator is skipped, because array assignment is not a common operation. 1266 This way, the critical lifecycle functions are available, with no growth in thunk creation. 1267 1268 Finally, the intuition that a programmer using an array always wants element constructors called \emph{automatically} is simplistic. 1269 Arrays exist to store different values at each position. 1270 So, an array initialization, in general, needs to let the prorammer provide different constructor arguments to each element. 1271 \begin{cfa} 1272 thread worker; 1273 void ?{}( worker & ) = void; 1274 void ?{}( worker &, int worker_num ); 1275 array(worker, 5) ws @= ???@; 1276 for (i; 5) ( ws[i] ){ @i@ }; 1277 \end{cfa} 1278 1279 Two \CFA features may, at first, seem viable for initializing the array @ws@, but on closer inspection, they are not. 1280 \begin{itemize} 1281 \item 1282 Empty initializer / default constructor: \lstinline{array(worker, 5) ws;}. 1283 This option does not work with the semantics of a thread: it has forked as soon as the constructor returns. 1284 The type does not provide a parameterless constructor because it is not possible to fork the thread until the @worker_num@ parameter is received. 1285 \item 1286 C initialization: \lstinline{array(worker, 5) ws AT EQUALS OPEN CLOSE;} (TODO: typeset at-equals). 1287 This option does achieve the desired semantics on the construction side. 1288 But destruction side, the desired semantics is for implicit destructor calls to continue, because with threads, the destructor is a join operation. 1289 C initialization disables \emph{all} implicit lifecycle management; here, the goal is to disable only the implicit construction. 1290 \end{itemize} 1291 1292 So, I enhanced the \CFA standard library to provide the missing semantics, available in either form: 1293 \begin{cfa} 1294 array(@uninit@(worker), 5) ws1; 1295 array(worker, 5) ws2 = { @delay_init@ }; 1296 \end{cfa} 1297 Both cause the @ws@-construction-time implicit call chain to stop before reaching a @worker@ constructor, while leaving the implicit destruction calls intact. 1298 Two forms are available, to parallel the development of this feature in \uCpp. 1299 Originally \uCpp offered only the @ws1@ form, there with the class-tempate @uNoCtor@ replacing \CFA's @uninit@. 1300 More recently, \uCpp got a declaration-helping macro, @uArray@, whose usage similar to the @ws2@ case. 1301 Based on results of piloting @uArray@ (as a replacement of @uNoCtor@), future work may choose to remove one of the options. 1302 1303 % note to Mike, I have more fragments on some details available in push24\fragments\uNoCtor.txt 1095 1304 1096 1305 \section{Comparison with other arrays}
Note: See TracChangeset
for help on using the changeset viewer.