Likes Dislike Improving Performance in C++ with Compile Time Polymorphism

来源:https://www.gamedev.net/resources/_/technical/general-programming/improving-performance-in-c-with-compile-time-polymorphism-r2015

Virtual functions are one of the most interesting and useful features of classes in C++. They allow for thinking of an object in terms of what type it is (Apple) as well as what category of types it belongs with (Fruit). Further, virtual functions allow for operating on objects in ways that respect their actual types while refering to them by category. However, the power and flexibility of virtual functions comes at a price that a good programmer must weigh against the benefits. Let’s take a quick look at using virtual functions and abstract base classes and from there examine a way in which we can improve program performance while retaining the power and flexbility of virtual functions.
Along with modeling different kinds of fruit and different kinds of animals, modeling different kinds of shapes accounts for most of the polymorphism examples found in C++ textbooks. More importantly, modeling different kinds of shapes readily lends itself to an area of game programming where improving performance is a high priority, namely graphics rendering. So modeling shapes will make a good basis for our examination.

Now, let’s begin.

class Shape
{
public:
  Shape()
  {
  }

  virtual ~Shape()
  {
  }

  virtual void DrawOutline() const = 0;
  virtual void DrawFill() const = 0;
};

class Rectangle : public Shape
{
public:
  Rectangle()
  {
  }

  virtual ~Rectangle()
  {
  }

  virtual void DrawOutline() const
  {
    ...
  }

  virtual void DrawFill() const
  {
    ...
  }
};

class Circle : public Shape
{
public:
  Circle()
  {
  }

  virtual ~Circle()
  {
  }

  virtual void DrawOutline() const
  {
    ...
  }

  virtual void DrawFill() const
  {
    ...
  }
};

All good so far, right? We can, for example, write…

Shape *myShape = new Rectangle;
myShape->DrawOutline();
delete myShape;

…and trust C++’s runtime polymorphism to decide that the myShape pointer actually points to aRectangle and that Rectangle’s DrawOutline() method should be called. If we wanted it to be a circle instead, we could just change “new Rectangle” to “new Circle”, andCircle’s DrawOutline() method would be called instead.

But wait a second. Thanks, C++, for the runtime polymorphism, but it’s pretty obvious from looking at that code thatmyShape is going to be a Rectangle; we don’t need fancy vtables to figure that out. Consider this code:

void DrawAShapeOverAndOver(Shape* myShape)
{
  for(int i=0; i<10000; i++)
  {
    myShape->DrawOutline();
  }
}

Shape *myShape = new Rectangle;
DrawAShapeOverAndOver(myShape);
delete myShape;

Look at what happens there! The program picks up myShape, inspects it, and says “Hmm, aRectangle. Ok.” Then it puts it down. Then it picks it up again. “Hmm. This time, it’s aRectangle. Ok. Hmm, and this time it’s a… Rectangle. Ok.” Repeat 9,997 times. Does all this type inspection eat up CPU cycles? Darn tootin’ it does. Although virtual function calls aren’t what you’d call slow, even a small delay really starts to add up when you’re doing it 10,000 times per object per frame. The real tragedy here is that weknow that the program doesn’t really need to check myShape’s type each time through the loop. “It’s always going to be the same thing!”, we shout at the compiler, “Just have the program look it up the first time!” For that matter, it doesn’t really need to be looked up the first time. Because we are calling it on aRectangle that we have just created, the object type is still going to be aRectangle when DrawAShapeOverAndOver() gets to it.

Let’s see if we can rewrite this function in a way that doesn’t require runtime lookups. We will make it specifically forRectangles, so we can just flat-out tell the dumb compiler what it is and forego the lookup code.

void DrawAShapeWhichIsARectangleOverAndOver(Shape* myShape)
{
  for(int i=0; i<10000; i++)
  {
    reinterpret_cast<Rectangle*>(myShape)->DrawOutline();
  }
}

Unfortunately, this doesn’t help one bit. Telling the compiler that the object is aRectangle isn’t enough. For all the compiler knows, the object could be a subclass ofRectangle. We still haven’t prevented the compiler from inserting runtime lookup code. To do that we must remove thevirtual keyword from the declaration of DrawOutline() and thereby change it into a non-virtual function. That means in turn, however, that we have to declare a separateDrawAShapeOverAndOver() for each and every subclass of Shape that we might want to draw. Alas, pursuing our desire for efficiency has driven us further and further away from our goal, to the point where there is barely any polymorphism left at all. So sad.

Thanks But No Thanks, C++

Reading over the last few paragraphs, the astute programmer will notice an interesting point: At no time did we actuallyneed runtime polymorphism. It helped us write our DrawAShapeOverAndOver() function by letting us write a single function that would work for all classes derived fromShape, but in each case the run-time lookup could have been done at compile-time.

Bearing this in mind, let’s approach polymorphism again, but this time with more caution. We won’t be making theDrawOutline() method virtual again, since so far that has done us no good at all. Instead, let’s rewriteDrawAShapeOverAndOver() as a templated function. This way we are not forced to write bothDrawAShapeWhichIsARectangleOverAndOver() and DrawAShapeWhichIsACircleOverAndOver().

template<typename ShapeType>
void DrawAShapeOverAndOver(ShapeType* myShape)
{
  for(int i=0; i<10000; i++)
  {
    myShape->DrawOutline();
  }
}

Rectangle *myRectangle = new Rectangle;
DrawAShapeOverAndOver(myRectangle);
delete myRectangle;

Hey! Now we’re getting somewhere! We can pass in any kind of Shape to DrawAShapeOverAndOver(), just like before, except this time there is no runtime checking ofmyShape’s type! Interestingly enough, Rectangle and Circle don’t even have to be derived fromShape. They just have to be classes with a DrawOutline() function.

Making Our Lives More Difficult

Let’s go back to our original example, but this time let’s make more use of the other features of subclassing. After all, derived classes and base classes with no private members, nontrivial constructors, or internal calls of virtual functions are a rather severe oversimplification of subclassing. Let’s also supply an actual implementation ofDrawOutline() and DrawFill(), albeit using a completely fictionalGraphics object that will nevertheless allow us to illustrate how functions in derived classes may use functions in base classes.

Now, let’s pull out the big guns.

class Shape
{
public:
  Shape(const Point &initialLocation,
     const std::string &initialOutlineColor,
     const std::string &initialFillColor) :
    location(initialLocation),
    outlineColor(initialOutlineColor),
    fillColor(initialFillColor)
  {
  }

  virtual ~Shape()
  {
  }

  virtual void DrawOutline() const = 0;
  virtual void DrawFill() const = 0;

  void SetOutlineColor(const std::string &newOutlineColor)
  {
    outlineColor = newOutlineColor;
  }

  void SetFillColor(const std::string &newFillColor)
  {
    fillColor = newFillColor;
  }

  void SetLocation(const Point & newLocation)
  {
    location = newLocation;
  }

  const std::string &GetOutlineColor() const
  {
    return outlineColor;
  }

  const std::string &GetFillColor() const
  {
    return fillColor;
  }

  const Point &GetLocation() const
  {
    return location;
  }

  void DrawFilled() const
  {
    DrawOutline();
    DrawFill();
  }

private:
  std::string outlineColor;

  std::string fillColor;

  Point location;
};

class Rectangle : public Shape
{
public:
  Rectangle(const Point &initialLocation,
       const std::string &initialOutlineColor,
       const std::string &initialFillColor(),
       double initialHeight,
       double initialWidth) :
    Shape(initialLocation, initialOutlineColor,
          initialFillColor),
    height(initialHeight),
    width(initialWidth)
  {
  }

  virtual ~Rectangle()
  {
  }

  virtual void DrawOutline() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawRectangleLines(height, width);
  }

  virtual void DrawFill() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawRectangleFill(height, width);
  }

  void SetHeight(double newHeight)
  {
    height = newHeight;
  }

  void SetWidth(double newWidth)
  {
    width = newWidth;
  }

  double GetHeight() const
  {
    return height;
  }

  double GetWidth() const
  {
    return width;
  }

private:
  double height;
  double width;
};

class Circle : public Shape
{
public:
  Circle(const Point &initialLocation,
      const std::string &initialOutlineColor,
      const std::string &initialFillColor,
      double initialRadius) :
    Shape(initialLocation, initialOutlineColor,
          initialFillColor),
    radius(initialRadius)
  {
  }

  virtual ~Circle()
  {
  }

  virtual void DrawOutline() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawCircularLine(radius);
  }

  virtual void DrawFill() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawCircularFill(radius);
  }

  void SetRadius(double newRadius)
  {
    radius = newRadius;
  }

  double GetRadius() const
  {
    return radius;
  }
private:
  double radius;
};

Whew! Let’s see what we added there. First of all, Shape objects now have data members. AllShape objects have a location, and an outlineColor and afillColor. In addition, Rectangle objects have a height and a width, andCircle objects have a radius. Each of these members has corresponding getter and setter functions. The most important new addition is theDrawFilled() method, which draws both the outline and the fill in one step by delegating these methods to the derived class.

We Can Rebuild It; We Have The Technology

Now that we have this all set up, let’s rip it apart! Let’s tear it down and rebuild it into a class structure which invites compile-time polymorphism.

How shall we do this? First, let’s remove the virtual keyword from the declarations ofDrawOutline() and DrawFill(). As we touched on earlier, virtual functions add runtime overhead which is precisely what we are trying to avoid. For that matter, let’s go one step further and remove the declarations of those functions from the base class altogether, as they do us no good anyway. Let’s leave them in as comments, though, so that it remains clear that they were omitted on purpose.

Now, what have we broken? Not much, actually. If we have a Rectangle, we can get and set its height and width and colors and location, and we can draw it. Life is good. However, one thing that we have broken is theDrawFilled() function, which calls the now nonexistent base class functionsDrawOutline() and DrawFill(). Base classes can only call functions of derived classes if those functions are declared as virtual in the base class–which is precisely what we do not want.

In order to fix the broken DrawFilled() function, we will use templates in a very strange and interesting way. Here’s a bit of code to broadly illustrate the insanity that is to come.

template <typename ShapeType>
class Shape
{

...
protected:
  Shape( ... )
  {
  }

};

class Rectangle : public Shape<Rectangle>
{

public:
  Rectangle( ... ) :
    Shape<Rectangle>( ... )
  {
  }
...

};

Whaaa? That’s right: Rectangle no longer inherits from Shape; now it inherits from a specialkind of Shape. Rectangle creates its own specialShape class, Shape, to inherit from. In fact, Rectangle is the only class that inherits from this specially craftedShape. To enforce this, we declare the constructor of the templatedShape class protected so that an object of this type can not be instanced directly. Instead, this special kind ofShape must be inherited from and instanced within the public constructor of the derived class.

Yes, it’s legal. Yes, it’s strange. Yes, it’s necessary. It’s called the “Curiously Recurring Template Pattern” (or Idiom, depending on who you ask).

But why? What could this strange code possibly gain us??

What we gain is the template parameter. The base class Shape now knows that it really is theShape part of a Rectangle because we have told it so through the template parameter, and because we have taken a solemn oath that the only class that ever inheritsShape is Rectangle. If Shape ever wonders what subclass it’s a part of, it can just check itsShapeType template parameter.

What this knowledge gains us, in turn, is the ability to downcast. Downcasting is taking an object of a base class and casting it as an object of a derived class. It’s whatdynamic_cast does for virtual classes, and it’s what virtual function calls do. It’s also what we tried to do way back near the beginning of this article, when we tried to usereinterpret_cast to convince our compiler that myShape was a Rectangle. Now that the functions aren’t virtual anymore, however, this will work much better (in other words, it’ll work). Let’s use it to rewriteDrawFilled().

template <typename ShapeType>
class Shape
{
  void DrawFilled()
  {
    reinterpret_cast<const ShapeType *>(this)->DrawOutline();
    reinterpret_cast<const ShapeType *>(this)->DrawFill();
  }
};

Take a moment to cogitate on this code. It’s possibly the most crucial part of this entire article. WhenDrawFilled() is called on a Rectangle, even though it is a method defined inShape and thus called with a this pointer of type Shape, it knows that it can safely treat itself as aRectangle. This lets Shape reinterpret_cast itself down to aRectangle and from there call DrawOutline() on the resultant Rectangle. Ditto with DrawFill().

Putting It Together

So let’s put it all together.

template<typename ShapeType>
class Shape
{
public:
  ~Shape()
  {
  }

/* Omitted from the base class and
   declared instead in subclasses */
/* void DrawOutline() const = 0; */
/* void DrawFill() const = 0; */

  void SetOutlineColor(const std::string &newOutlineColor)
  {
    outlineColor = newOutlineColor;
  }

  void SetFillColor(const std::string &newFillColor)
  {
    fillColor = newFillColor;
  }

  void SetLocation(const Point &newLocation)
  {
    location = newLocation;
  }

  const std::string &GetOutlineColor() const
  {
    return outlineColor;
  }

  const std::string &GetFillColor() const
  {
    return fillColor;
  }

  const Point &GetLocation() const
  {
    return location;
  }

  void DrawFilled() const
  {
    reinterpret_cast<const ShapeType *>(this)->DrawOutline();
    reinterpret_cast<const ShapeType *>(this)->DrawFill();
  }

protected:
  Shape(const Point &initialLocation,
     const std::string &initialOutlineColor,
     const std::string &initialFillColor) :
    location(initialLocation),
    outlineColor(initialOutlineColor),
    fillColor(initialFillColor)
  {
  }

private:
  std::string outlineColor;

  std::string fillColor;

  Point location;
};

class Rectangle : public Shape<Rectangle>
{
public:
  Rectangle(const Point &initialLocation,
        const std::string &initialOutlineColor,
        const std::string &initialFillColor,
        double initialHeight,
        double initialWidth) :
    Shape<Rectangle>(initialLocation, initialOutlineColor,
                     initialFillColor),
    height(initialHeight),
    width(initialWidth)
  {
  }

  ~Rectangle()
  {
  }

  void DrawOutline() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawRectangleLines(height, width);
  }

  void DrawFill() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawRectangleFill(height, width);
  }

  void SetHeight(double newHeight)
  {
    height = newHeight;
  }

  void SetWidth(double newWidth)
  {
    width = newWidth;
  }

  double GetHeight() const
  {
    return height;
  }

  double GetWidth() const
  {
    return width;
  }

private:
  double height;
  double width;
};

class Circle : public Shape<Circle>
{
public:
  Circle(const Point &initialLocation,
      const std::string &initialOutlineColor,
      const std::string &initialFillColor,
      double initialRadius) :
    Shape<Circle>(initialLocation, initialOutlineColor,
                  initialFillColor),
    radius(initialRadius)
  {
  }

  ~Circle()
  {
  }

  void DrawOutline() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawCircularLine(radius);
  }

  void DrawFill() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawCircularFill(radius);
  }

  void SetRadius(double newRadius)
  {
    radius = newRadius;
  }

  double GetRadius() const
  {
    return radius;
  }
private:
  double radius;
};

This is just what we need! Base class functions can defer certain functionality to derived classes and derived classes can decide which base class functions to override. If we had declared anon-virtual DrawOutline() function in Shape (rather than leaving it in only as a comment), it would be optional forCircle and Rectangle to override it. This approach allows programmers using a class to not concern themselves with whether a function is in the derived class or inherited from the base class. It’s the functionality that we had in the last section, but without the added overhead of run-time polymorphism.

While we’re at it, let’s rewrite DrawAShapeOverAndOver().

template<typename ShapeType>
void DrawAShapeOverAndOver(ShapeType* myShape)
{
  for(int i=0; i<10000; i++)
  {
    myShape->DrawOutline();
    // OR
    myShape->DrawFilled();
  }
}

Rectangle *rectangle = new Rectangle;
DrawAShapeOverAndOver(rectangle);
delete rectangle;

Notice that we can call member functions declared either in the derived class or the base class. Of course, if the templated function uses member functions defined in only a particular derived class (such asGetRadius()), the templated function will not compile if used with a class that does not have those member functions. For example, callingGetRadius() on a Rectangle will not compile.

Limitations

The biggest limitation of compile-time polymorphism is that it’s compile-time. In other words, if we want to call a function on aRectangle, we can’t do it through a pointer to a Shape. In fact, there is no such thing as a pointer to aShape, since there is no Shape class without a template argument.

This is less of a limitation than you might think. Take another look at our rewrittenDrawAShapeOverAndOver():

template<typename ShapeType>
void DrawAShapeOverAndOver(ShapeType* myShape)
{
  for(int i=0; i<10000; i++)
  {
    myShape->DrawOutline();
  }
}

Essentially, wherever you once had functions that took in base class pointers, you now have templated functions that take in derived class pointers (or derived classes). The responsibility for calling the correct member function is delegated to the outer templated function, not to the object.

Templates have drawbacks. Although the best way to get a feel for these drawbacks is to experience them yourself, it’s also a good idea for a programmer to have an idea of what to expect. First and foremost is that most compilers require templates to be declared inline. This means that all your templated functions will have to go in the header, which can make your code less tidy. (If you’re using the Comeau compiler, this doesn’t apply to you. Congratulations.)

Secondly, templates can lead to code bloat, since different versions of the functions must be compiled for each datatype they are used with. Howmuch code bloat is caused is very specific to the project; switching all of my content loading functions to use this model increased my stripped executable size by about 50k. As always, the best source of wisdom is your own tests.

Summary

Using templates for compile-time polymorphism can increase performance when they are used to avoid needless virtual function binding. With careful design, templates can be used to give non-virtual classes all the capabilities that virtual classes have, except for runtime binding. Although such compile-time polymorphism is not appropriate for every situation, a careful decision by the programmer as to where virtual functions are actually needed can dramatically improve code performance, without incurring a loss of flexibility or readability.

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值