title: Large (0,1) matrix multiplication using bitwise AND and popcount instead of actual int or float multiplies?
body: <p>For multiplying large binary matrices (10Kx20K), what I usually to do is to convert the matrices to float ones and perform float matrix multiplication as integer matrix multiplication is pretty slow (<a href="https://stackoverflow.com/questions/45373679/why-is-it-faster-to-perform-float-by-float-matrix-multiplication-compared-to-int">have a look at here</a>).</p>
  
  <p>This time though, I'd need to perform over hundred thousands of these multiplications and <strong>even a millisecond performance improvement on average matters to me</strong>.</p>
  
  <hr>
  
  <p>I want an <code>int</code> or <code>float</code> matrix as a result, because the product may have elements that aren't 0 or 1.  The input matrix elements are all 0 or 1, so they can be stored as single bits.</p>
  
  <p>In the inner-product between a row vector and a column vector (to produce one element of the output matrix), multiplication simplifies to bitwise AND.  Addition is still addition, but we can add bits with a population-count function instead of looping over them individually.</p>
  
  <p>Some other boolean/binary-matrix functions OR the bits instead of counting them, producing a bit-matrix result, but that's not what I need.</p>
  
  <hr>
  
  <p>Here is a sample code showing that forming the problem as <code>std::bitset</code>,  <code>AND</code> and <code>count</code> operations is faster than matrix multiplication.</p>
  
  <pre><code>#include &lt;iostream&gt;
  using std::cout; using std::endl;
  #include &lt;vector&gt;
      using std::vector;
  #include &lt;chrono&gt;
  #include &lt;Eigen/Dense&gt;
      using Eigen::Map; using Eigen::Matrix; using Eigen::MatrixXf;
  #include &lt;random&gt;
      using std::random_device; using std::mt19937; using std::uniform_int_distribution;
  #include &lt;bitset&gt;
      using std::bitset;
  
  using std::floor;
  
  const int NROW = 1000;
  const int NCOL = 20000;
  
  const float DENSITY = 0.4;
  const float DENOMINATOR = 10.0 - (10*DENSITY);
  
  void fill_random(vector&lt;float&gt;&amp; vec) {
      random_device rd;
      mt19937 eng(rd());
      uniform_int_distribution&lt;&gt; distr(0, 10);
      int nnz = 0;
      for (int i = 0; i &lt; NROW*NCOL; ++i)
          vec.push_back(floor(distr(eng)/DENOMINATOR));
  }
  
  void matmul(vector&lt;float&gt;&amp; vec){
      float *p = vec.data();
      MatrixXf A = Eigen::Map&lt;Eigen::Matrix&lt;float, NROW, NCOL, Eigen::RowMajor&gt;&gt;(p);
      cout &lt;&lt; "Eigen matrix has " &lt;&lt; A.rows() &lt;&lt; " rows and " &lt;&lt; A.cols() &lt;&lt; " columns." &lt;&lt; endl;
      cout &lt;&lt; "Total non-zero values : " &lt;&lt; A.sum() &lt;&lt; endl;
      cout &lt;&lt; "The density of non-zero values is " &lt;&lt;  A.sum() * 1.0 / (A.cols()*A.rows()) &lt;&lt; endl;
  
      auto start = std::chrono::steady_clock::now();
      MatrixXf B = A.transpose() * A;
      auto end = (std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(std::chrono::steady_clock::now() - start)).count();
      cout &lt;&lt; "Mat mul took " &lt;&lt; end &lt;&lt; " ms"&lt;&lt; endl;
  
      // Just to make sure the operation is not skipped by compiler
      cout &lt;&lt; "Eigen coo ";
      for (int i=0; i&lt;10; ++i)
          cout &lt;&lt; B(0,i) &lt;&lt; " ";
      cout &lt;&lt; endl;
  }
  
  
  void bitset_op(vector&lt;float&gt;&amp; vec) {
      // yeah it's not a great idea to set size at compile time but have to
      vector&lt;bitset&lt;NROW&gt;&gt; col_major(NCOL);
  
      // right, multiple par for isn't a good idea, maybe in a parallel block
      // Doing this for simplicity to profile second loop timing 
      // converting row major float vec to col major bool vec
      #pragma omp parallel for
      for (int j=0; j &lt; NCOL; ++j) {
          for (int i=0; i &lt; NROW; ++i) {
              col_major[j].set(i, vec[i*NCOL + j] &amp;&amp; 1);
          }
      }
  
      auto start = std::chrono::steady_clock::now();
      vector&lt;int&gt; coo;
      coo.assign(NCOL*NCOL, 0);
      #pragma omp parallel for
      for (int j=0; j &lt; NCOL; ++j) {
          for (int k=0; k&lt;NCOL; ++k) {
              coo[j*NCOL + k] = (col_major[j]&amp;col_major[k]).count();
          }
      }
      auto end = (std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(std::chrono::steady_clock::now() - start)).count();
      cout &lt;&lt; "bitset intersection took " &lt;&lt; end &lt;&lt; " ms"&lt;&lt; endl;
  
      // Just to make sure the operation is not skipped by compiler
      cout &lt;&lt; "biset coo ";
      for (int i=0; i&lt;10; ++i)
          cout &lt;&lt; coo[i] &lt;&lt; " ";
      cout &lt;&lt; endl;
  }
  
  
  int main() {
      // Saving to float instead of int to speed up matmul
      vector&lt;float&gt; vec;
      fill_random(vec);
      matmul(vec);
      bitset_op(vec);
  }
  </code></pre>
  
  <p>Running this with:</p>
  
  <pre><code>g++ -O3 -fopenmp -march=native -I. -std=c++11 code.cpp -o code
  </code></pre>
  
  <p>I get:</p>
  
  <pre><code>Eigen matrix has 1000 rows and 20000 columns.
  Total non-zero values : 9.08978e+06
  The density of non-zero values is 0.454489
  Mat mul took 1849 ms
  Eigen coo 458 206 208 201 224 205 204 199 217 210
  bitset intersection took 602 ms
  biset coo 458 206 208 201 224 205 204 199 217 210
  </code></pre>
  
  <p>As you can see, matmul as set of bitset operations is about 3x faster than Eigen's float matmul, which makes sense.</p>
  
  <p><strong>I want to emphasize that I need to perform this operation over 100K</strong> (in the HPC or cloud) and a millisecond performance improvement on average would make a difference. </p>
  
  <p>I'm not bound to any specific library, C++ standard, etc. So please feel free to answer with any solution that you think is faster other than those using GPU, as I can't use it for a number of reasons.</p>