So, I’ve already written that bitwise operations is a very fast thing. But how can this be used in application ? Let’s say, you need to iterate over the powerset of some elements which can be useful in various fields(for example):

  • operations with graphs;
  • dynamic programming problems, where all combinations of elements should be stored in memoization table;
  • other combinatorial problems…

  There are two approaches for generating powerset: recursion and bitwise operations. Which of them performs better? Let’s test.

class PowerSet(
val set: IntArray,
) {
val subSets = mutableListOf<IntArray>()

    fun generateRecursive(at: Int, used: BooleanArray) {
        val n = set.size;
        val range = set.indices
        if (at == n) {

            // Print found subset!
            var subSet = IntArray(n)
            for (i in range) {
                if (used[i]) {
                    subSet[i] = set[i]
                }
            }
            subSets.add(subSet)
        } else {

            // Include this element
            used[at] = true
            generateRecursive(at + 1, used)

            // Backtrack and don't include this element
            used[at] = false
            generateRecursive(at + 1, used)
        }
    }

    fun generateBinary() {
        val n = set.size;
        val range = set.indices
        val maxVal = 1 shl n;

        for (subset in 0 until maxVal) {
            var subSet = IntArray(n)
            for (i in range) {
                val mask = 1 shl i
                if ((subset and mask) == mask) {
                    subSet[i] = set[i]
                }
            }
            subSets.add(subSet)
        }
    }

}
  
@State(Scope.Benchmark)
class PowerSetBenchmark {
@Param("3", "4", "5", "10", "20", "26")
var setSize = 0
lateinit var set: IntArray

    @Setup
    fun setUp() {
        set = IntArray(setSize) { i -> i }
    }


    @Benchmark
    fun powSetRecursive() {
        val ps = PowerSet(set)
        ps.generateRecursive(
            0, BooleanArray(setSize)
        )

    }

    @Benchmark
    fun powSetBinary() {
        val ps = PowerSet(set)
        ps.generateBinary()
    }

}

Source code of PowerSet class and benchmark

Results

Benchmark                          (setSize)  Mode  Cnt        Score         Error  Units
PowerSetBenchmark.powSetBinary             3  avgt    3        0.060 ±       0.065  us/op
PowerSetBenchmark.powSetBinary             4  avgt    3        0.137 ±       0.046  us/op
PowerSetBenchmark.powSetBinary             5  avgt    3        0.286 ±       0.064  us/op
PowerSetBenchmark.powSetBinary             9  avgt    3        6.654 ±       6.681  us/op
PowerSetBenchmark.powSetBinary            10  avgt    3       14.016 ±      18.000  us/op
PowerSetBenchmark.powSetBinary            11  avgt    3       82.002 ±      63.015  us/op
PowerSetBenchmark.powSetBinary            20  avgt    3    92927.224 ±   28005.295  us/op
PowerSetBenchmark.powSetBinary            26  avgt    3  8389329.133 ± 2665432.289  us/op
PowerSetBenchmark.powSetRecursive          3  avgt    3        0.076 ±       0.017  us/op
PowerSetBenchmark.powSetRecursive          4  avgt    3        0.191 ±       0.087  us/op
PowerSetBenchmark.powSetRecursive          5  avgt    3        0.400 ±       0.071  us/op
PowerSetBenchmark.powSetRecursive          9  avgt    3        7.283 ±       0.500  us/op
PowerSetBenchmark.powSetRecursive         10  avgt    3       20.408 ±      33.346  us/op
PowerSetBenchmark.powSetRecursive         11  avgt    3       57.855 ±      15.787  us/op
PowerSetBenchmark.powSetRecursive         20  avgt    3    82136.368 ±   75290.816  us/op
PowerSetBenchmark.powSetRecursive         26  avgt    3  7995903.683 ± 2029323.013  us/op

  As you can notice from benchmark summary binary implementation is faster with n <= 10, but with n=11 recursive implementation becomes faster.
  For me its pretty unexpected result which I cannot explain at the moment. So much the better, another interesting topic to explore is put to the piggy-bank :-) See you!