It’s common practice for API designers providing operator overloads to follow the advice given in the Framework Design Guidelinesand also provide methods with friendly names for the same operations. For example, the designers of the Vector3D structure provided a Multiply method and a Multiply operator to support multiplying a vector by a scalar. Why do they do this? Well, you can’t guarantee that all .NET programming languages will support operator overloading because it is not part of the Common Language Specification. If you use C#, which does support operator overloading, then you’re free to choose either the operators or the methods as there’s no difference between them, right? Well a problem I encountered recently made me stop and think about operator overloads, and how it can be difficult to keep track of the number of temporary objects being created. Consider the following snippet:
private readonly List<Point3D> m_points; private readonly List<Vector3D> m_velocities; public void UpdatePoints(double variance, double ellapsedTime) { Debug.Assert( m_points.Count == m_velocities.Count, "Invalid collections.", "Fields 'm_points' and 'm_velocities' should have equal counts."); for (int i = 0; i < m_points.Count; i++) { m_points[i] -= m_velocities[i] * variance * ellapsedTime; } }
How many Vector3D objects are being created each time through the loop? Let's re-write the snippet to emphasise the temporary objects.
private readonly List<Point3D> m_points; private readonly List<Vector3D> m_velocities; public void UpdatePoints(double variance, double ellapsedTime) { Debug.Assert( m_points.Count == m_velocities.Count, "Invalid collections.", "Fields 'm_points' and 'm_velocities' should have equal counts."); for (int i = 0; i < m_points.Count; i++) { Vector3D tmp1 = vectors[i] * variance; Vector3D tmp2 = tmp1 * ellapsedTime; m_points[i] -= tmp2; } }
This makes it much easier to spot that two temporary Vector3D objects are being created for the vector-scalar multiplications, i.e. one of them is unnecessary because we can combine the scalar values. Compare this with the original snippet re-written again to use the Multiply method instead of the operator overload.
private readonly List<Point3D> m_points; private readonly List<Vector3D> m_velocities; public void UpdatePoints(double variance, double ellapsedTime) { Debug.Assert( m_points.Count == m_velocities.Count, "Invalid collections.", "Fields 'm_points' and 'm_velocities' should have equal counts."); for (int i = 0; i < m_points.Count; i++) { m_points[i] -= Vector3D.Multiply( Vector3D.Multiply(m_velocities[i], variance), ellapsedTime); } }
I think it's now obvious that we're performing two vector-scalar multiplications. In fact, had I used the Multiply method initially I'm certain I would have automatically used only one Multiply call as follows:
private readonly List<Point3D> m_points; private readonly List<Vector3D> m_velocities; public void UpdatePoints(double variance, double ellapsedTime) { Debug.Assert( m_points.Count == m_velocities.Count, "Invalid collections.", "Fields 'm_points' and 'm_velocities' should have equal counts."); for (int i = 0; i < m_points.Count; i++) { m_points[i] -= Vector3D.Multiply(m_velocities[i], variance * ellapsedTime); } }
But is creating a few too many temporary Vector3D objects that bad? Well in this scenario it depends on how many loops you perform and, even more importantly, what platform you're targeting. The impact for code running on desktops/servers will probably be negligible but I was certainly surprised by the difference these changes made on mobile/embedded devices. As a result I've begun to favour the clarity of methods over the terseness of operator overloads. There’s no guarantee that this will always provide any noticeable performance improvements but in my opinion the methods are closer to the pit of success than the operator overloads. Note that the standard performance advice still applies, which is measure, measure, measure J.
0 comments:
Post a Comment