Search code examples
performancec++11move-constructormove-assignment-operator

How to take advantage of the Move Semantics for a better performance in C++11?


After many trials I still do not understand how to properly take advantage of the move semantics in order to not copy the result of the operation and just use the pointer, or std::move, to "exchange" the data pointed to. This will be very usefull to speed-up more complicated functions like f(g(),h(i(l,m),n(),p(q())) The objective is to have:

t3={2,4,6}; 
t1={}; // empty

While executing the code below the output is:

t3={2,4,6};
t1={1,2,3};

Code:

namespace MTensor {

 typedef std::vector<double> Tensor1DType;

 class Tensor1D {
  private:
    //std::shared_ptr<Tensor1DType> data = std::make_shared<Tensor1DType>();
    Tensor1DType * data = new Tensor1DType;
  public:
    Tensor1D() {
  };
  Tensor1D(const Tensor1D& other) {
    for(int i=0;i<other.data->size();i++) {
      data->push_back(other.data->at(i));
    }
  }
  Tensor1D(Tensor1D&& other) : data(std::move(other.data)) {
    other.data = nullptr;
  }
  ~Tensor1D() {
    delete data;
  };
  int size() {
    return data->size();
  };
  void insert(double value) {
    data->push_back(value);
  }
  void insert(const std::initializer_list<double>&  valuesList) {
    for(auto value : valuesList) {
      data->push_back(value);
    }
  }
  double operator() (int i) {
    if(i>data->size()) {
      std::cout << "index must be within vector dimension" << std::endl;
      exit(1);
    }
    return data->at(i);
  }
  Tensor1D& operator=(Tensor1D&& other)  {
    if (this == &other){
      return *this;
    }
    data = other.data;
    other.data = nullptr;
    return *this;
  }
  void printTensor(Tensor1DType info) {
    for(int i=0;i<info.size();i++) {
      std::cout << info.at(i) << "," << std::endl;
    }
  }
  void printTensor() {
    for(int i=0;i<data->size();i++) {
      std::cout << data->at(i) << "," << std::endl;
    }
  }
};
} // end of namespace MTensor

In file main.cpp:

MTensor::Tensor1D scalarProduct1D(MTensor::Tensor1D t1, double scalar) {
  MTensor::Tensor1D tensor;
    for(int i=0;i<t1.size();++i) {
      tensor.insert(t1(i) * scalar);
    }
  //return std::move(tensor);
  return tensor;
}

int main() {
  MTensor::Tensor1D t1;
  t1.insert({1,2,3});
  std::cout << "t1:" << std::endl;
  t1.printTensor();
  MTensor::Tensor1D t3(scalarProduct1D(t1,2));
  std::cout << "t3:" << std::endl;
  t3.printTensor();
  std::cout << "t1:" << std::endl;
  t1.printTensor();
  return 0;
}

Solution

  • Your use of new is a red flag, especially on a std::vector.

    std::vectors support move semantics natively. They are a memory management class. Manual memory management of a memory management class is a BIG red flag.

    Follow the rule of 0. =default your move constructor, move assignment, copy constructor, destructor and copy assignment. Remove the * from the vector. Don't allocate it. Replace data-> with data.

    The second thing you should do is change:

    MTensor::Tensor1D scalarProduct1D(MTensor::Tensor1D t1, double scalar) {
    

    As it stands you take the first argument by value. That is great.

    But once you take it by value, you should reuse it! Return t1 instead of creating a new temporary and returning it.

    For that to be efficient, you will want to have a way to modify a tensor in-place.

    void set(int i, double v) {
      if(i>data->size()) {
        std::cout << "index must be within vector dimension" << std::endl;
        exit(1);
      }
      data.at(i) = v;
    }
    

    which gives us:

    MTensor::Tensor1D scalarProduct1D(MTensor::Tensor1D t1, double scalar) {
      for(int i=0;i<t1.size();++i) {
        ts.set(i, t1(i) * scalar);
      }
      return t1; // implicitly moved
    }
    

    We are now getting close.

    The final thing you have to do is this:

    MTensor::Tensor1D t3(scalarProduct1D(std::move(t1),2));
    

    to move the t1 into the scalarProduct1D.

    A final problem with your code is that you use at and you check bounds. at's purpose is to check bounds. If you use at, don't check bounds (do so with a try/catch). If you check bounds, use [].

    End result:

    typedef std::vector<double> Tensor1DType;
    
    class Tensor1D {
    private:
      //std::shared_ptr<Tensor1DType> data = std::make_shared<Tensor1DType>();
      Tensor1DType data;
    public:
      Tensor1D() {};
      Tensor1D(const Tensor1D& other)=default;
      Tensor1D(Tensor1D&& other)=default;
      ~Tensor1D()=default;
      Tensor1D& operator=(Tensor1D&& other)=default; 
      Tensor1D& operator=(Tensor1D const& other)=default; 
      Tensor1D(const std::initializer_list<double>&  valuesList) {
        insert(valuesList);
      }
      int size() const {
        return data.size();
      };
      void insert(double value) {
        data.push_back(value);
      }
      void insert(const std::initializer_list<double>&  valuesList) {
        data.insert( data.end(), valuesList.begin(), valuesList.end() );
      }
      double operator() (int i) const {
        if(i>data.size()) {
          std::cout << "index must be within vector dimension" << std::endl;
          exit(1);
        }
        return data[i];
      }
      void set(int i, double v) {
        if(i>data->size()) {
          std::cout << "index must be within vector dimension" << std::endl;
          exit(1);
        }
        data.at(i) = v;
      }
      static void printTensor(Tensor1DType const& info) {
        for(double e : info) {
          std::cout << e << "," << std::endl;
        }
      }
      void printTensor() const {
        printTensor(data);
      }
    };
    
    MTensor::Tensor1D scalarProduct1D(MTensor::Tensor1D t1, double scalar) {
      for(int i=0;i<t1.size();++i) {
        t1.set(i, t1(i) * scalar);
      }
      return t1;
    }
    
    
    
    int main() {
      MTensor::Tensor1D t1 = {1,2,3};
      std::cout << "t1:" << std::endl;
      t1.printTensor();
      MTensor::Tensor1D t3(scalarProduct1D(std::move(t1),2));
      std::cout << "t3:" << std::endl;
      t3.printTensor();
      std::cout << "t1:" << std::endl;
      t1.printTensor();
      return 0;
    }
    

    with a few other minor fixes (like using range-for, DRY, etc).