Search code examples
c++memorytemporary-objects

Minimizing peak memory usage while performing a series of operations on std containers


I have a "pipeline" of functions that produce a final result along with some intermediate results. I am looking for a way to reduce the peak memory by cleaning up the intermediate results as soon as they are no longer needed.

This is the way it looks if I don't care about memory of intermediate results. B, C, D, E, and F are all std containers with over a million objects each.

void A::getF(F &f) {
  B b;
  C c;
  computeBAndC(b, c);
  D d;
  computeD(b, c, d);
  // b and c no longer needed
  E e;
  computeE(d, e);
  // d no longer needed
  computeF(e, f);
}

These are the ways I came up with:

A. Use new and delete to cleanup once they are no longer needed. But, does it make sense to use new and delete for std containers?

void A::getF(F &f) {
  B *b = new B;
  C *c = new C;
  computeBAndC(b, c);
  D *d = new D;
  computeD(b, c, d);
  // b and c no longer needed
  delete b;
  delete c;
  E e;
  computeE(d, e);
  // d no longer needed
  delete d;
  computeF(e, f);
}

B. Use the std container's swap function to clear their memory after use.

void A::getF(F &f) {
  B b;
  C c;
  computeBAndC(b, c);
  D d;
  computeD(b, c, d);
  // b and c no longer needed
  B().swap(b);
  C().swap(c);
  E e;
  computeE(d, e);
  // d no longer needed
  D().swap(d);
  computeF(e, f);
}

C. Use blocks.

void A::computeF(F &f) {
  E e;
  {
    D d;
    {
      B b;
      C c;
      computeBAndC(b, c);
      computeD(b, c, d);
      // b and c no longer needed
    }
    computeE(d, e);
    // d no longer needed
  }
  computeF(e, f);
}

D. Restructure so that the intermediate results get deleted at the end of function scope:

void A::getF(F &f) {
  E e;
  getE(e);
  computeF(e, f);
}

void A::getE(E &e) {
  D d;
  getD(d);
  computeE(d, e);
}

void A::getD(D &d) {
  B b;
  C c;
  computeBAndC(b, c);
  computeD(b, c, d);
}

What are the pros and cons of these approaches? Will these really reduce peak memory usage? Is there a better way?


Solution

  • Honestly, I would choose the option D because given that there's some connection between some of your variables, extracting this code into functions not only would address your memory concerns, but would also better document your intentions using your code itself (I believe you could come up with good self-explanatory names in your code). The only issue is of course the performance if you use a nicer approach to the function signatures (assuming you don't control the compute functions: i.e.

    E A::computeE(const D& d) {
        E e;
        computeE(d, e);
        return E;
    }
    

    I understand from your comment you can only use C++98 which doesn't have move semantics. However, the compilers have long been able to perform the named return value optimization - and in your case I would try to test the abilities of your compiler to use NRVO (and copy elision), and if it's good - use a more natural (IMO) signature I outlined - otherwise I'd use your option D.