Optimizing for speed. Moving skewness finder. Cumulative sum proving to be bottleneck.
3 views (last 30 days)
Show older comments
Hello,
I have written the below provided function to calculate the moving window skewness of a time series.
To find out where the bottleneck is I used tic/toc and it seems the part where the cumulative sum (T2) is calculated takes the most amount of time.
The fraction of time taken (for each section) depends on N, the size of the time series. This can be found out using the attached script (Trial.m). For N = 1000000 the cumsum takes around 83% of the time.
The concern:
How can I speed up this code? Or has it reached its limit?
function sk_timeseries = moving_skewness(x, window_size, window_step)
% T1 start
X1 = x;
X2 = X1 .* x;
X3 = X2 .* x;
% T1 end
% T2 start
S1 = cumsum([0, X1]);
S2 = cumsum([0, X2]);
S3 = cumsum([0, X3]);
% T2 end
% T3 start
% Get the indices at which to calculate skewness values
window_ends_idx = window_size: window_step: length(x);
% Preallocate array for storing skewness time series
sk_timeseries = zeros(1, length(window_ends_idx));
N = window_size;
% T3 end
% T4 start
% Generate skewness time series
for k = 1: length(window_ends_idx)
j = window_ends_idx(k) + 1;
i = j - window_size;
% Calculate M1_ji
M1_ji = (S1(j) - S1(i)) / N;
% Calculate necessary central moments
M2_ji = (S2(j) - S2(i)) - window_size * M1_ji * M1_ji;
M3_ji = (S3(j) - S3(i)) - 3 * M1_ji * (S2(j) - S2(i)) + 2 * N * M1_ji * M1_ji * M1_ji;
% Calculate skewness
sk_timeseries(k) = sqrt(N) * M3_ji / (M2_ji^1.5);
end
% T4 end
end
2 Comments
Bruno Luong
on 25 Aug 2023
The concatenation with 0 takes as much as the cumsum
N = 1000000;
X1 = rand(1,N);
tic
X1 = [0 X1];
toc
tic
S1 = cumsum(X1);
toc
dpb
on 25 Aug 2023
function sk_timeseries = moving_skewness(x, window_size, window_step)
% T1 start
X1 = [0 x];
...
end
would at least remove two of those catenation operations...and
N = 1000000;
X1 = rand(1,N);
tic
X1 = [0 X1];
toc
X1 = rand(1,N+1);
tic
X1(1)=0;
toc
would eliminate needing either if could generate x with the extra position to begin with. Clearing the one location is much cheaper than needing to allocate the new array [] requires.
Of course, if generating the initial array is just adding a [0 ...] somewhere else it doesn't help unless it can be moved out of the function and only done once't.
Accepted Answer
Bruno Luong
on 26 Aug 2023
Edited: Bruno Luong
on 26 Aug 2023
Speed optimized code changes are:
- Vectorize for-loop
- Avoid padding 0 by treating the first "iteration" in separate code.
- Mutualize the quantities that are repeatly computed several times
- Replace power 1.5 (using costly exponential) with x*sqrt(x)
The optimized code doesn't look very maintainable friendly. Not spectacular but surely faster.
clear
N = 1000000;
window_size = floor(N * 0.05);
window_step = floor(window_size * (1 / 100));
x = randi([1e3 1e4],[1 N]);
r1 = moving_skewness(x, window_size, window_step);
r2 = moving_skewness_BLU(x, window_size, window_step);
close all
plot(r1)
hold on
plot(r2)
t_org = timeit(@()moving_skewness(x, window_size, window_step),1); % 7.93 ms
t_BLU = timeit(@()moving_skewness_BLU(x, window_size, window_step),1); % 5.48 ms
t_org_ms = 1000*t_org,
t_BLU_ms = 1000*t_BLU,
function sk_timeseries = moving_skewness(x, window_size, window_step)
% T1 start
X1 = x;
X2 = X1 .* x;
X3 = X2 .* x;
% T1 end
% T2 start
S1 = cumsum([0, X1]);
S2 = cumsum([0, X2]);
S3 = cumsum([0, X3]);
% T2 end
% T3 start
% Get the indices at which to calculate skewness values
window_ends_idx = window_size: window_step: length(x);
% Preallocate array for storing skewness time series
sk_timeseries = zeros(1, length(window_ends_idx));
N = window_size;
% T3 end
% T4 start
% Generate skewness time series
for k = 1: length(window_ends_idx)
j = window_ends_idx(k) + 1;
i = j - window_size;
% Calculate M1_ji
M1_ji = (S1(j) - S1(i)) / N;
% Calculate necessary central moments
M2_ji = (S2(j) - S2(i)) - window_size * M1_ji * M1_ji;
M3_ji = (S3(j) - S3(i)) - 3 * M1_ji * (S2(j) - S2(i)) + 2 * N * M1_ji * M1_ji * M1_ji;
% Calculate skewness
sk_timeseries(k) = sqrt(N) * M3_ji / (M2_ji^1.5);
end
% T4 end
end
function sk_timeseries = moving_skewness_BLU(x, window_size, window_step)
X2 = x .* x;
S1 = cumsum(x,2);
S2 = cumsum(X2,2);
S3 = cumsum(X2.*x,2);
N = window_size;
j = N: window_step: length(x);
i = j - N;
i(1) = j(1); % dummy
M1_ji = S1(j) - S1(i);
M1_ji2 = M1_ji .* M1_ji;
dS2 = S2(j) - S2(i);
M2_ji = dS2 - 1/N * M1_ji2;
M3_ji = (S3(j) - S3(i)) + M1_ji .* ((-3/N)*dS2 + (2/(N*N))*M1_ji2);
sk_timeseries = sqrt(N) * M3_ji ./ (M2_ji.*sqrt(M2_ji));
M1_ji = S1(N);
M1_ji2 = M1_ji .* M1_ji;
dS2 = S2(N);
M2_ji = dS2 - 1/N * M1_ji2;
M3_ji = S3(N) + M1_ji .* ((-3/N)*dS2 + (2/(N*N))*M1_ji2);
sk_timeseries(1) = sqrt(N) * M3_ji ./ (M2_ji.*sqrt(M2_ji));
end
1 Comment
Bruno Luong
on 30 Aug 2023
Edited: Bruno Luong
on 30 Aug 2023
You can replace
X2 = x .* x;
S1 = cumsum(x,2);
S2 = cumsum(X2,2);
S3 = cumsum(X2.*x,2);
% by
[S1, S2, S3] = cumsum3(x);
with this mexfile (needed to be compiled). It is slighly faster according to my test.
/**************************************************************************
* Matlab Mex file cumsum3.c
*
* [S1, S2, S3] = cumsum(x)
* for x double vector array performs
*
* S1 = cumsum(x);
* S2 = cumsum(x.^2);
* S3 = cumsum(x.^3);
*
* [S1, S2, S3] = cumsum(x, true) pads 0 in the head of S1, S2 and S3
*
* mex -O -R2018a cumsum3.c
*************************************************************************/
#include "mex.h"
#include "matrix.h"
#define X prhs[0]
#define PAD0 prhs[1]
#define S1 plhs[0]
#define S2 plhs[1]
#define S3 plhs[2]
/* Gateway of SUM1 */
void mexFunction(int nlhs, mxArray *plhs[],
int nrhs, const mxArray *prhs[]) {
mwSize i, M , N, MS, NS;
int Pad0;
double *xptr, *s1ptr, *s2ptr, *s3ptr;
double x, x2, s1, s2, s3;
M = mxGetM(X);
N = mxGetN(X);
xptr = mxGetPr(X);
Pad0 = nrhs >= 2 ? (int)mxGetScalar(PAD0) : 0;
if (N == 1)
{
NS = N;
MS = Pad0 ? M+1 : M;
}
else
{
MS = M;
NS = Pad0 ? N+1 : N;
}
S1 = mxCreateDoubleMatrix(MS, NS, mxREAL);
S2 = mxCreateDoubleMatrix(MS, NS, mxREAL);
S3 = mxCreateDoubleMatrix(MS, NS, mxREAL);
if (S1 == NULL || S2 == NULL || S3 == NULL)
{
mexErrMsgTxt("cumsum3: Out of memory");
}
s1ptr = mxGetDoubles(S1);
s2ptr = mxGetDoubles(S2);
s3ptr = mxGetDoubles(S3);
M *= N;
s1 = s2 = s3 = 0;
if (Pad0)
{
*s1ptr = *s2ptr = *s2ptr = 0;
s1ptr++;
s2ptr++;
s3ptr++;
}
for (i=0; i<M; i++)
{
*s1ptr++ = (s1 += (x = *xptr++));
*s2ptr++ = (s2 += (x2 = x*x));
*s3ptr++ = (s3 += (x2*x));
}
}
Such function is useful for various scenarios of running stats with 3rd order moment.
The MATLAB function is
function sk_timeseries = moving_skewness_BLU2(x, window_size, window_step)
[S1, S2, S3] = cumsum3(x,1);
N = window_size;
j = N+1: window_step: length(x)+1;
i = j - N;
M1_ji = S1(j) - S1(i);
M1_ji2 = M1_ji .* M1_ji;
dS2 = S2(j) - S2(i);
M2_ji = dS2 - 1/N * M1_ji2;
M3_ji = (S3(j) - S3(i)) + M1_ji .* ((-3/N)*dS2 + (2/(N*N))*M1_ji2);
sk_timeseries = sqrt(N) * M3_ji ./ (M2_ji.*sqrt(M2_ji));
end
More Answers (0)
See Also
Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!