So after I posted the question last night I came up with a solution that is fast and vectorized: (note that if you're working with huge numbers you'll need to remove the N
for accuracy, but you'll incur a huge speed penalty)
gifs[inds_, strides : {__Integer}] :=
Module[
{
accstr,
stride = strides,
ind = inds - 1,
moddable,
modres
},
accstr =
N@
Append[
Reverse@FoldList[Times, strides[[-1 ;; 2 ;; -1]]],
1
];
moddable = If[ListQ@inds, Map[ind/# &, accstr], ind/accstr];
modres = 1 + Mod[Floor[moddable], stride];
If[ListQ@inds, Transpose, Identity]@modres
]
Obligatory performance comparison:
tests = RandomInteger[{1, 60}, 100];
res = gifs[tests, {5, 4, 3}]; // RepeatedTiming // First
0.000053
res == gridIndex[tests, {5, 4, 3}]
True
gridIndex[tests, {5, 4, 3}]; // RepeatedTiming // First
0.0064
getIndex[tests, {5, 4, 3}]; // RepeatedTiming // First
0.00032
unrankList[{5, 4, 3}][tests]; // RepeatedTiming // First
0.00039
getInds[tests, {5, 4, 3}]; // RepeatedTiming // First
0.000063
Clearly vectorization is doing what it should and getting us the performance we'd expect (which is interestingly better than a compiled implementation on my machine)
Here's a more detailed performance analysis which shows we're long-term a little bit better than Mathematica's auto-parallelization in compiled functions:
RandomSeed[123];
shape = RandomInteger[{1, 10}, 4];
sizes = {1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 75000,
100000, 125000, 150000, 500000};
idxs = RandomInteger[{1, Times @@ shape}, #] & /@ sizes;
testC =
MapThread[
{#, getInds[#2, shape]; // RepeatedTiming // First} &,
{
sizes,
idxs
}
];
testU =
MapThread[
{#, gifs[#2, shape]; // RepeatedTiming // First} &,
{
sizes,
idxs
}
];
ListLinePlot[
{
testC,
testU
},
PlotLegends -> {"getInds", "gifs"},
PlotRange -> All
]
