Skip to content

Commit 6907614

Browse files
authored
Merge pull request #3409 from josdejong/deepMap-perforance-fix-3
Increase performance of `collection` deepMap and deepForEach with DenseMatrices
2 parents b5773df + 2f62fda commit 6907614

File tree

7 files changed

+109
-44
lines changed

7 files changed

+109
-44
lines changed

src/function/matrix/filter.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,11 @@ export const createFilter = /* #__PURE__ */ factory(name, dependencies, ({ typed
6666
*/
6767
function _filterCallback (x, callback) {
6868
const fastCallback = optimizeCallback(callback, x, 'filter')
69+
if (fastCallback.isUnary) {
70+
return filter(x, fastCallback.fn)
71+
}
6972
return filter(x, function (value, index, array) {
7073
// invoke the callback function with the right number of arguments
71-
return fastCallback(value, [index], array)
74+
return fastCallback.fn(value, [index], array)
7275
})
7376
}

src/function/matrix/forEach.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@ export const createForEach = /* #__PURE__ */ factory(name, dependencies, ({ type
5252
* @private
5353
*/
5454
function _forEach (array, callback) {
55-
deepForEach(array, optimizeCallback(callback, array, name))
55+
const fastCallback = optimizeCallback(callback, array, name)
56+
deepForEach(array, fastCallback.fn, fastCallback.isUnary)
5657
}

src/function/matrix/map.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const createMap = /* #__PURE__ */ factory(name, dependencies, ({ typed })
151151
* @private
152152
*/
153153
function _mapArray (array, callback) {
154-
return deepMap(array, optimizeCallback(callback, array, name))
154+
const fastCallback = optimizeCallback(callback, array, name)
155+
return deepMap(array, fastCallback.fn, fastCallback.isUnary)
155156
}
156157
})

src/type/matrix/DenseMatrix.js

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -529,44 +529,55 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies
529529
* Applies a callback function to a reference to each element of the matrix
530530
* @memberof DenseMatrix
531531
* @param {Function} callback The callback function is invoked with three
532-
* parameters: an array, an integer index to that
533-
* array, and the Matrix being traversed.
532+
* parameters: the array containing the element,
533+
* the index of the element within that array (as an integer),
534+
* and for non unarry callbacks copy of the current index (as an array of integers).
534535
*/
535536
DenseMatrix.prototype._forEach = function (callback) {
536-
const me = this
537-
const s = me.size()
538-
const maxDepth = s.length - 1
537+
const isUnary = callback.length === 2 // callback has 2 parameters: value, index
538+
const maxDepth = this._size.length - 1
539539

540-
if (maxDepth < 0) {
540+
if (maxDepth < 0) return
541+
542+
if (isUnary) {
543+
iterateUnary(this._data)
541544
return
542545
}
543546

544547
if (maxDepth === 0) {
545-
const thisSize = s[0]
546-
for (let i = 0; i < thisSize; i++) {
547-
callback(me._data, i, [i])
548+
for (let i = 0; i < this._data.length; i++) {
549+
callback(this._data, i, [i])
548550
}
549551
return
550552
}
551553

552-
const index = Array(s.length)
554+
const index = new Array(maxDepth + 1)
553555

554-
function recurse (data, depth) {
555-
const thisSize = s[depth]
556+
iterate(this._data)
557+
function iterate (data, depth = 0) {
556558
if (depth < maxDepth) {
557-
for (let i = 0; i < thisSize; i++) {
559+
for (let i = 0; i < data.length; i++) {
558560
index[depth] = i
559-
recurse(data[i], depth + 1)
561+
iterate(data[i], depth + 1)
560562
}
561563
} else {
562-
for (let i = 0; i < thisSize; i++) {
564+
for (let i = 0; i < data.length; i++) {
563565
index[depth] = i
564566
callback(data, i, index.slice())
565567
}
566568
}
567569
}
568-
569-
recurse(me._data, 0)
570+
function iterateUnary (data, depth = 0) {
571+
if (depth < maxDepth) {
572+
for (let i = 0; i < data.length; i++) {
573+
iterateUnary(data[i], depth + 1)
574+
}
575+
} else {
576+
for (let i = 0; i < data.length; i++) {
577+
callback(data, i)
578+
}
579+
}
580+
}
570581
}
571582

572583
/**
@@ -576,17 +587,21 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies
576587
* @param {Function} callback The callback function is invoked with three
577588
* parameters: the value of the element, the index
578589
* of the element, and the Matrix being traversed.
590+
* @param {boolean} skipZeros If true, the callback function is invoked only for non-zero entries
591+
* @param {boolean} isUnary If true, the callback function is invoked with one parameter
579592
*
580593
* @return {DenseMatrix} matrix
581594
*/
582-
DenseMatrix.prototype.map = function (callback) {
595+
DenseMatrix.prototype.map = function (callback, skipZeros = false, isUnary = false) {
583596
const me = this
584597
const result = new DenseMatrix(me)
585-
const fastCallback = optimizeCallback(callback, me._data, 'map')
598+
const fastCallback = optimizeCallback(callback, me._data, 'map', isUnary)
586599

587-
result._forEach(function (arr, i, index) {
588-
arr[i] = fastCallback(arr[i], index, me)
589-
})
600+
const applyCallback = isUnary || fastCallback.isUnary
601+
? (arr, i) => { arr[i] = fastCallback.fn(arr[i]) }
602+
: (arr, i, index) => { arr[i] = fastCallback.fn(arr[i], index, me) }
603+
604+
result._forEach(applyCallback)
590605

591606
return result
592607
}
@@ -597,13 +612,18 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies
597612
* @param {Function} callback The callback function is invoked with three
598613
* parameters: the value of the element, the index
599614
* of the element, and the Matrix being traversed.
615+
* @param {boolean} skipZeros If true, the callback function is invoked only for non-zero entries
616+
* @param {boolean} isUnary If true, the callback function is invoked with one parameter
600617
*/
601-
DenseMatrix.prototype.forEach = function (callback) {
618+
DenseMatrix.prototype.forEach = function (callback, skipZeros = false, isUnary = false) {
602619
const me = this
603-
const fastCallback = optimizeCallback(callback, me._data, 'map')
604-
me._forEach(function (arr, i, index) {
605-
fastCallback(arr[i], index, me)
606-
})
620+
const fastCallback = optimizeCallback(callback, me._data, 'map', isUnary)
621+
622+
const applyCallback = isUnary || fastCallback.isUnary
623+
? (arr, i) => { fastCallback.fn(arr[i]) }
624+
: (arr, i, index) => { fastCallback.fn(arr[i], index, me) }
625+
626+
me._forEach(applyCallback)
607627
}
608628

609629
/**

src/type/matrix/SparseMatrix.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie
857857
// invoke callback
858858
const invoke = function (v, i, j) {
859859
// invoke callback
860-
return fastCallback(v, [i, j], me)
860+
return fastCallback.fn(v, [i, j], me)
861861
}
862862
// invoke _map
863863
return _map(this, 0, rows - 1, 0, columns - 1, invoke, skipZeros)
@@ -976,7 +976,8 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie
976976
const i = this._index[k]
977977

978978
// value @ k
979-
fastCallback(this._values[k], [i, j], me)
979+
// TODO apply a non indexed version of algorithm in case fastCallback is not optimized
980+
fastCallback.fn(this._values[k], [i, j], me)
980981
}
981982
} else {
982983
// create a cache holding all defined values
@@ -990,7 +991,7 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie
990991
// and either read the value or zero
991992
for (let i = 0; i < rows; i++) {
992993
const value = (i in values) ? values[i] : 0
993-
fastCallback(value, [i, j], me)
994+
fastCallback.fn(value, [i, j], me)
994995
}
995996
}
996997
}

src/utils/collection.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function containsCollections (array) {
2727
*/
2828
export function deepForEach (array, callback) {
2929
if (isMatrix(array)) {
30-
array.forEach(x => callback(x))
30+
array.forEach(x => callback(x), false, true)
3131
} else {
3232
arrayDeepForEach(array, callback, true)
3333
}
@@ -48,14 +48,14 @@ export function deepForEach (array, callback) {
4848
export function deepMap (array, callback, skipZeros) {
4949
if (!skipZeros) {
5050
if (isMatrix(array)) {
51-
return array.map(x => callback(x))
51+
return array.map(x => callback(x), false, true)
5252
} else {
5353
return arrayDeepMap(array, callback, true)
5454
}
5555
}
5656
const skipZerosCallback = (x) => x === 0 ? x : callback(x)
5757
if (isMatrix(array)) {
58-
return array.map(x => skipZerosCallback(x))
58+
return array.map(x => skipZerosCallback(x), false, true)
5959
} else {
6060
return arrayDeepMap(array, skipZerosCallback, true)
6161
}

src/utils/optimizeCallback.js

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ import { typeOf as _typeOf } from './is.js'
88
* @param {Function} callback The original callback function to simplify.
99
* @param {Array|Matrix} array The array that will be used with the callback function.
1010
* @param {string} name The name of the function that is using the callback.
11+
* @param {boolean} [isUnary=false] If true, the callback function is unary and will be optimized as such.
1112
* @returns {Function} Returns a simplified version of the callback function.
1213
*/
13-
export function optimizeCallback (callback, array, name) {
14+
export function optimizeCallback (callback, array, name, isUnary = false) {
1415
if (typed.isTypedFunction(callback)) {
15-
const firstIndex = (array.isMatrix ? array.size() : arraySize(array)).map(() => 0)
16-
const firstValue = array.isMatrix ? array.get(firstIndex) : get(array, firstIndex)
17-
const numberOfArguments = _findNumberOfArguments(callback, firstValue, firstIndex, array)
16+
let numberOfArguments
17+
if (isUnary) {
18+
numberOfArguments = 1
19+
} else {
20+
const firstIndex = (array.isMatrix ? array.size() : arraySize(array)).map(() => 0)
21+
const firstValue = array.isMatrix ? array.get(firstIndex) : get(array, firstIndex)
22+
numberOfArguments = _findNumberOfArgumentsTyped(callback, firstValue, firstIndex, array)
23+
}
1824
let fastCallback
1925
if (array.isMatrix && (array.dataType !== 'mixed' && array.dataType !== undefined)) {
2026
const singleSignature = _findSingleSignatureWithArity(callback, numberOfArguments)
@@ -23,11 +29,18 @@ export function optimizeCallback (callback, array, name) {
2329
fastCallback = callback
2430
}
2531
if (numberOfArguments >= 1 && numberOfArguments <= 3) {
26-
return (...args) => _tryFunctionWithArgs(fastCallback, args.slice(0, numberOfArguments), name, callback.name)
32+
return {
33+
isUnary: numberOfArguments === 1,
34+
fn: (...args) => _tryFunctionWithArgs(fastCallback, args.slice(0, numberOfArguments), name, callback.name)
35+
}
2736
}
28-
return (...args) => _tryFunctionWithArgs(fastCallback, args, name, callback.name)
37+
return { isUnary: false, fn: (...args) => _tryFunctionWithArgs(fastCallback, args, name, callback.name) }
38+
}
39+
if (isUnary === undefined) {
40+
return { isUnary: _findIfCallbackIsUnary(callback), fn: callback }
41+
} else {
42+
return { isUnary, fn: callback }
2943
}
30-
return callback
3144
}
3245

3346
function _findSingleSignatureWithArity (callback, arity) {
@@ -42,7 +55,33 @@ function _findSingleSignatureWithArity (callback, arity) {
4255
}
4356
}
4457

45-
function _findNumberOfArguments (callback, value, index, array) {
58+
/**
59+
* Determines if a given callback function is unary (i.e., takes exactly one argument).
60+
*
61+
* This function checks the following conditions to determine if the callback is unary:
62+
* 1. The callback function should have exactly one parameter.
63+
* 2. The callback function should not use the `arguments` object.
64+
* 3. The callback function should not use rest parameters (`...`).
65+
* If in doubt, this function shall return `false` to be safe
66+
*
67+
* @param {Function} callback - The callback function to be checked.
68+
* @returns {boolean} - Returns `true` if the callback is unary, otherwise `false`.
69+
*/
70+
function _findIfCallbackIsUnary (callback) {
71+
if (callback.length !== 1) return false
72+
73+
const callbackStr = callback.toString()
74+
// Check if the callback function uses `arguments`
75+
if (/arguments/.test(callbackStr)) return false
76+
77+
// Extract the parameters of the callback function
78+
const paramsStr = callbackStr.match(/\(.*?\)/)
79+
// Check if the callback function uses rest parameters
80+
if (/\.\.\./.test(paramsStr)) return false
81+
return true
82+
}
83+
84+
function _findNumberOfArgumentsTyped (callback, value, index, array) {
4685
const testArgs = [value, index, array]
4786
for (let i = 3; i > 0; i--) {
4887
const args = testArgs.slice(0, i)

0 commit comments

Comments
 (0)