You can pack and unpack non-binary values in bitfields without slow multiplication, division or modulo ops.
An integer has 32 bits and those bits can be isolated in any way you want.
So lets say you have 16 integer parameters (a thru p), all of which you can guarrantee will be between the values of 0 and 3 (inclusive). You can pack all 16 parameters into a single integer using bitfield operations. Here's how you'd pack them:
integer packed;
packed = ((a & 3) << 30) | ((b & 3) << 28) | ((c & 3) << 26) ... etc
((a & 3) << 30) basically masks out everything but the least significant 2 bits, and then shifts them up 30 bits into the highest 2 bit positions. ((b & 3) << 2

shifts the lowest 2 bits up 28 bits. Then when you OR the two values together, this packs them together in one integer.
In other words: the decimal value of 3 is equivalent to the binary value
00000000000000000000000000000011b
ANDing it with another integer will isolate the lowest two bits
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxb
AND 00000000000000000000000000000011b
---------------------------------------------------
000000000000000000000000000000xxb
Shift right by 30 bits to get
xx000000000000000000000000000000b
Repeat with the next value but this time shift right by 28 bits to get
00yy0000000000000000000000000000b
Then you OR the two results together to get
xx000000000000000000000000000000b
OR 00yy0000000000000000000000000000b
--------------------------------------------------
xxyy0000000000000000000000000000b
You can then unpack them like this:
on_rez(integer packed)
{
a = ((packed >> 30) & 3);
b = ((packed >> 28) & 3);
c = ((packed >> 26) & 3);
// etc ...
}
You could even use a list and a loop to achieve this, although I suspect that unrolling the loop would in fact be faster and more memory efficient given what we know about list operations.
There is nothing to say that you can't use different sized bit fields if you have parameters that have other precision requirements.