compile.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. "use strict"
  2. var uniq = require("uniq")
  3. // This function generates very simple loops analogous to how you typically traverse arrays (the outermost loop corresponds to the slowest changing index, the innermost loop to the fastest changing index)
  4. // TODO: If two arrays have the same strides (and offsets) there is potential for decreasing the number of "pointers" and related variables. The drawback is that the type signature would become more specific and that there would thus be less potential for caching, but it might still be worth it, especially when dealing with large numbers of arguments.
  5. function innerFill(order, proc, body) {
  6. var dimension = order.length
  7. , nargs = proc.arrayArgs.length
  8. , has_index = proc.indexArgs.length>0
  9. , code = []
  10. , vars = []
  11. , idx=0, pidx=0, i, j
  12. for(i=0; i<dimension; ++i) { // Iteration variables
  13. vars.push(["i",i,"=0"].join(""))
  14. }
  15. //Compute scan deltas
  16. for(j=0; j<nargs; ++j) {
  17. for(i=0; i<dimension; ++i) {
  18. pidx = idx
  19. idx = order[i]
  20. if(i === 0) { // The innermost/fastest dimension's delta is simply its stride
  21. vars.push(["d",j,"s",i,"=t",j,"p",idx].join(""))
  22. } else { // For other dimensions the delta is basically the stride minus something which essentially "rewinds" the previous (more inner) dimension
  23. vars.push(["d",j,"s",i,"=(t",j,"p",idx,"-s",pidx,"*t",j,"p",pidx,")"].join(""))
  24. }
  25. }
  26. }
  27. if (vars.length > 0) {
  28. code.push("var " + vars.join(","))
  29. }
  30. //Scan loop
  31. for(i=dimension-1; i>=0; --i) { // Start at largest stride and work your way inwards
  32. idx = order[i]
  33. code.push(["for(i",i,"=0;i",i,"<s",idx,";++i",i,"){"].join(""))
  34. }
  35. //Push body of inner loop
  36. code.push(body)
  37. //Advance scan pointers
  38. for(i=0; i<dimension; ++i) {
  39. pidx = idx
  40. idx = order[i]
  41. for(j=0; j<nargs; ++j) {
  42. code.push(["p",j,"+=d",j,"s",i].join(""))
  43. }
  44. if(has_index) {
  45. if(i > 0) {
  46. code.push(["index[",pidx,"]-=s",pidx].join(""))
  47. }
  48. code.push(["++index[",idx,"]"].join(""))
  49. }
  50. code.push("}")
  51. }
  52. return code.join("\n")
  53. }
  54. // Generate "outer" loops that loop over blocks of data, applying "inner" loops to the blocks by manipulating the local variables in such a way that the inner loop only "sees" the current block.
  55. // TODO: If this is used, then the previous declaration (done by generateCwiseOp) of s* is essentially unnecessary.
  56. // I believe the s* are not used elsewhere (in particular, I don't think they're used in the pre/post parts and "shape" is defined independently), so it would be possible to make defining the s* dependent on what loop method is being used.
  57. function outerFill(matched, order, proc, body) {
  58. var dimension = order.length
  59. , nargs = proc.arrayArgs.length
  60. , blockSize = proc.blockSize
  61. , has_index = proc.indexArgs.length > 0
  62. , code = []
  63. for(var i=0; i<nargs; ++i) {
  64. code.push(["var offset",i,"=p",i].join(""))
  65. }
  66. //Generate loops for unmatched dimensions
  67. // The order in which these dimensions are traversed is fairly arbitrary (from small stride to large stride, for the first argument)
  68. // TODO: It would be nice if the order in which these loops are placed would also be somehow "optimal" (at the very least we should check that it really doesn't hurt us if they're not).
  69. for(var i=matched; i<dimension; ++i) {
  70. code.push(["for(var j"+i+"=SS[", order[i], "]|0;j", i, ">0;){"].join("")) // Iterate back to front
  71. code.push(["if(j",i,"<",blockSize,"){"].join("")) // Either decrease j by blockSize (s = blockSize), or set it to zero (after setting s = j).
  72. code.push(["s",order[i],"=j",i].join(""))
  73. code.push(["j",i,"=0"].join(""))
  74. code.push(["}else{s",order[i],"=",blockSize].join(""))
  75. code.push(["j",i,"-=",blockSize,"}"].join(""))
  76. if(has_index) {
  77. code.push(["index[",order[i],"]=j",i].join(""))
  78. }
  79. }
  80. for(var i=0; i<nargs; ++i) {
  81. var indexStr = ["offset"+i]
  82. for(var j=matched; j<dimension; ++j) {
  83. indexStr.push(["j",j,"*t",i,"p",order[j]].join(""))
  84. }
  85. code.push(["p",i,"=(",indexStr.join("+"),")"].join(""))
  86. }
  87. code.push(innerFill(order, proc, body))
  88. for(var i=matched; i<dimension; ++i) {
  89. code.push("}")
  90. }
  91. return code.join("\n")
  92. }
  93. //Count the number of compatible inner orders
  94. // This is the length of the longest common prefix of the arrays in orders.
  95. // Each array in orders lists the dimensions of the correspond ndarray in order of increasing stride.
  96. // This is thus the maximum number of dimensions that can be efficiently traversed by simple nested loops for all arrays.
  97. function countMatches(orders) {
  98. var matched = 0, dimension = orders[0].length
  99. while(matched < dimension) {
  100. for(var j=1; j<orders.length; ++j) {
  101. if(orders[j][matched] !== orders[0][matched]) {
  102. return matched
  103. }
  104. }
  105. ++matched
  106. }
  107. return matched
  108. }
  109. //Processes a block according to the given data types
  110. // Replaces variable names by different ones, either "local" ones (that are then ferried in and out of the given array) or ones matching the arguments that the function performing the ultimate loop will accept.
  111. function processBlock(block, proc, dtypes) {
  112. var code = block.body
  113. var pre = []
  114. var post = []
  115. for(var i=0; i<block.args.length; ++i) {
  116. var carg = block.args[i]
  117. if(carg.count <= 0) {
  118. continue
  119. }
  120. var re = new RegExp(carg.name, "g")
  121. var ptrStr = ""
  122. var arrNum = proc.arrayArgs.indexOf(i)
  123. switch(proc.argTypes[i]) {
  124. case "offset":
  125. var offArgIndex = proc.offsetArgIndex.indexOf(i)
  126. var offArg = proc.offsetArgs[offArgIndex]
  127. arrNum = offArg.array
  128. ptrStr = "+q" + offArgIndex // Adds offset to the "pointer" in the array
  129. case "array":
  130. ptrStr = "p" + arrNum + ptrStr
  131. var localStr = "l" + i
  132. var arrStr = "a" + arrNum
  133. if (proc.arrayBlockIndices[arrNum] === 0) { // Argument to body is just a single value from this array
  134. if(carg.count === 1) { // Argument/array used only once(?)
  135. if(dtypes[arrNum] === "generic") {
  136. if(carg.lvalue) {
  137. pre.push(["var ", localStr, "=", arrStr, ".get(", ptrStr, ")"].join("")) // Is this necessary if the argument is ONLY used as an lvalue? (keep in mind that we can have a += something, so we would actually need to check carg.rvalue)
  138. code = code.replace(re, localStr)
  139. post.push([arrStr, ".set(", ptrStr, ",", localStr,")"].join(""))
  140. } else {
  141. code = code.replace(re, [arrStr, ".get(", ptrStr, ")"].join(""))
  142. }
  143. } else {
  144. code = code.replace(re, [arrStr, "[", ptrStr, "]"].join(""))
  145. }
  146. } else if(dtypes[arrNum] === "generic") {
  147. pre.push(["var ", localStr, "=", arrStr, ".get(", ptrStr, ")"].join("")) // TODO: Could we optimize by checking for carg.rvalue?
  148. code = code.replace(re, localStr)
  149. if(carg.lvalue) {
  150. post.push([arrStr, ".set(", ptrStr, ",", localStr,")"].join(""))
  151. }
  152. } else {
  153. pre.push(["var ", localStr, "=", arrStr, "[", ptrStr, "]"].join("")) // TODO: Could we optimize by checking for carg.rvalue?
  154. code = code.replace(re, localStr)
  155. if(carg.lvalue) {
  156. post.push([arrStr, "[", ptrStr, "]=", localStr].join(""))
  157. }
  158. }
  159. } else { // Argument to body is a "block"
  160. var reStrArr = [carg.name], ptrStrArr = [ptrStr]
  161. for(var j=0; j<Math.abs(proc.arrayBlockIndices[arrNum]); j++) {
  162. reStrArr.push("\\s*\\[([^\\]]+)\\]")
  163. ptrStrArr.push("$" + (j+1) + "*t" + arrNum + "b" + j) // Matched index times stride
  164. }
  165. re = new RegExp(reStrArr.join(""), "g")
  166. ptrStr = ptrStrArr.join("+")
  167. if(dtypes[arrNum] === "generic") {
  168. /*if(carg.lvalue) {
  169. pre.push(["var ", localStr, "=", arrStr, ".get(", ptrStr, ")"].join("")) // Is this necessary if the argument is ONLY used as an lvalue? (keep in mind that we can have a += something, so we would actually need to check carg.rvalue)
  170. code = code.replace(re, localStr)
  171. post.push([arrStr, ".set(", ptrStr, ",", localStr,")"].join(""))
  172. } else {
  173. code = code.replace(re, [arrStr, ".get(", ptrStr, ")"].join(""))
  174. }*/
  175. throw new Error("cwise: Generic arrays not supported in combination with blocks!")
  176. } else {
  177. // This does not produce any local variables, even if variables are used multiple times. It would be possible to do so, but it would complicate things quite a bit.
  178. code = code.replace(re, [arrStr, "[", ptrStr, "]"].join(""))
  179. }
  180. }
  181. break
  182. case "scalar":
  183. code = code.replace(re, "Y" + proc.scalarArgs.indexOf(i))
  184. break
  185. case "index":
  186. code = code.replace(re, "index")
  187. break
  188. case "shape":
  189. code = code.replace(re, "shape")
  190. break
  191. }
  192. }
  193. return [pre.join("\n"), code, post.join("\n")].join("\n").trim()
  194. }
  195. function typeSummary(dtypes) {
  196. var summary = new Array(dtypes.length)
  197. var allEqual = true
  198. for(var i=0; i<dtypes.length; ++i) {
  199. var t = dtypes[i]
  200. var digits = t.match(/\d+/)
  201. if(!digits) {
  202. digits = ""
  203. } else {
  204. digits = digits[0]
  205. }
  206. if(t.charAt(0) === 0) {
  207. summary[i] = "u" + t.charAt(1) + digits
  208. } else {
  209. summary[i] = t.charAt(0) + digits
  210. }
  211. if(i > 0) {
  212. allEqual = allEqual && summary[i] === summary[i-1]
  213. }
  214. }
  215. if(allEqual) {
  216. return summary[0]
  217. }
  218. return summary.join("")
  219. }
  220. //Generates a cwise operator
  221. function generateCWiseOp(proc, typesig) {
  222. //Compute dimension
  223. // Arrays get put first in typesig, and there are two entries per array (dtype and order), so this gets the number of dimensions in the first array arg.
  224. var dimension = (typesig[1].length - Math.abs(proc.arrayBlockIndices[0]))|0
  225. var orders = new Array(proc.arrayArgs.length)
  226. var dtypes = new Array(proc.arrayArgs.length)
  227. for(var i=0; i<proc.arrayArgs.length; ++i) {
  228. dtypes[i] = typesig[2*i]
  229. orders[i] = typesig[2*i+1]
  230. }
  231. //Determine where block and loop indices start and end
  232. var blockBegin = [], blockEnd = [] // These indices are exposed as blocks
  233. var loopBegin = [], loopEnd = [] // These indices are iterated over
  234. var loopOrders = [] // orders restricted to the loop indices
  235. for(var i=0; i<proc.arrayArgs.length; ++i) {
  236. if (proc.arrayBlockIndices[i]<0) {
  237. loopBegin.push(0)
  238. loopEnd.push(dimension)
  239. blockBegin.push(dimension)
  240. blockEnd.push(dimension+proc.arrayBlockIndices[i])
  241. } else {
  242. loopBegin.push(proc.arrayBlockIndices[i]) // Non-negative
  243. loopEnd.push(proc.arrayBlockIndices[i]+dimension)
  244. blockBegin.push(0)
  245. blockEnd.push(proc.arrayBlockIndices[i])
  246. }
  247. var newOrder = []
  248. for(var j=0; j<orders[i].length; j++) {
  249. if (loopBegin[i]<=orders[i][j] && orders[i][j]<loopEnd[i]) {
  250. newOrder.push(orders[i][j]-loopBegin[i]) // If this is a loop index, put it in newOrder, subtracting loopBegin, to make sure that all loopOrders are using a common set of indices.
  251. }
  252. }
  253. loopOrders.push(newOrder)
  254. }
  255. //First create arguments for procedure
  256. var arglist = ["SS"] // SS is the overall shape over which we iterate
  257. var code = ["'use strict'"]
  258. var vars = []
  259. for(var j=0; j<dimension; ++j) {
  260. vars.push(["s", j, "=SS[", j, "]"].join("")) // The limits for each dimension.
  261. }
  262. for(var i=0; i<proc.arrayArgs.length; ++i) {
  263. arglist.push("a"+i) // Actual data array
  264. arglist.push("t"+i) // Strides
  265. arglist.push("p"+i) // Offset in the array at which the data starts (also used for iterating over the data)
  266. for(var j=0; j<dimension; ++j) { // Unpack the strides into vars for looping
  267. vars.push(["t",i,"p",j,"=t",i,"[",loopBegin[i]+j,"]"].join(""))
  268. }
  269. for(var j=0; j<Math.abs(proc.arrayBlockIndices[i]); ++j) { // Unpack the strides into vars for block iteration
  270. vars.push(["t",i,"b",j,"=t",i,"[",blockBegin[i]+j,"]"].join(""))
  271. }
  272. }
  273. for(var i=0; i<proc.scalarArgs.length; ++i) {
  274. arglist.push("Y" + i)
  275. }
  276. if(proc.shapeArgs.length > 0) {
  277. vars.push("shape=SS.slice(0)") // Makes the shape over which we iterate available to the user defined functions (so you can use width/height for example)
  278. }
  279. if(proc.indexArgs.length > 0) {
  280. // Prepare an array to keep track of the (logical) indices, initialized to dimension zeroes.
  281. var zeros = new Array(dimension)
  282. for(var i=0; i<dimension; ++i) {
  283. zeros[i] = "0"
  284. }
  285. vars.push(["index=[", zeros.join(","), "]"].join(""))
  286. }
  287. for(var i=0; i<proc.offsetArgs.length; ++i) { // Offset arguments used for stencil operations
  288. var off_arg = proc.offsetArgs[i]
  289. var init_string = []
  290. for(var j=0; j<off_arg.offset.length; ++j) {
  291. if(off_arg.offset[j] === 0) {
  292. continue
  293. } else if(off_arg.offset[j] === 1) {
  294. init_string.push(["t", off_arg.array, "p", j].join(""))
  295. } else {
  296. init_string.push([off_arg.offset[j], "*t", off_arg.array, "p", j].join(""))
  297. }
  298. }
  299. if(init_string.length === 0) {
  300. vars.push("q" + i + "=0")
  301. } else {
  302. vars.push(["q", i, "=", init_string.join("+")].join(""))
  303. }
  304. }
  305. //Prepare this variables
  306. var thisVars = uniq([].concat(proc.pre.thisVars)
  307. .concat(proc.body.thisVars)
  308. .concat(proc.post.thisVars))
  309. vars = vars.concat(thisVars)
  310. if (vars.length > 0) {
  311. code.push("var " + vars.join(","))
  312. }
  313. for(var i=0; i<proc.arrayArgs.length; ++i) {
  314. code.push("p"+i+"|=0")
  315. }
  316. //Inline prelude
  317. if(proc.pre.body.length > 3) {
  318. code.push(processBlock(proc.pre, proc, dtypes))
  319. }
  320. //Process body
  321. var body = processBlock(proc.body, proc, dtypes)
  322. var matched = countMatches(loopOrders)
  323. if(matched < dimension) {
  324. code.push(outerFill(matched, loopOrders[0], proc, body)) // TODO: Rather than passing loopOrders[0], it might be interesting to look at passing an order that represents the majority of the arguments for example.
  325. } else {
  326. code.push(innerFill(loopOrders[0], proc, body))
  327. }
  328. //Inline epilog
  329. if(proc.post.body.length > 3) {
  330. code.push(processBlock(proc.post, proc, dtypes))
  331. }
  332. if(proc.debug) {
  333. console.log("-----Generated cwise routine for ", typesig, ":\n" + code.join("\n") + "\n----------")
  334. }
  335. var loopName = [(proc.funcName||"unnamed"), "_cwise_loop_", orders[0].join("s"),"m",matched,typeSummary(dtypes)].join("")
  336. var f = new Function(["function ",loopName,"(", arglist.join(","),"){", code.join("\n"),"} return ", loopName].join(""))
  337. return f()
  338. }
  339. module.exports = generateCWiseOp