Alex Fabijanic uncovers the internals of DynamicAny with some performance and size tests.
In the first installment of this article, Poco::DynamicAny class was presented, along with rationale for it as well as practical usage examples. The convenient advantages, such as direct dynamically typed mapping from an external data source to the program storage were described. Like boost::any , DynamicAny readily provides storage and value extraction of an arbitrary user defined data type. The most common data types conversions are supported out-of-the-box through specializations provided by POCO framework, while the ones for user defined types can be added through value holder template specialization. In this installment, we delve into the internals of the class and run some tests to compare DynamicAny to similar C++ data type conversion facilities.
Performance
It is well known that there is no such thing as a free lunch. DynamicAny shall clearly pay a hefty price in CPU cycle currency for its flexibility and safety. But how well does DynamicAny perform compared to bare-bone static C++ casts and other dynamic typing solutions?
Two types of tests were performed:
- conversion ( Int32 in, double out; Int32 in, Uint16 out; std::string in, double out)
- extraction ( double in, double out; std::string in, std::string out)
Various Any extractions are compared with the DynamicAny extraction. As described in the first part of the article, Any is not capable of doing conversions. For conversions, we are comparing DynamicAny with boost::lexical_cast . The test code is shown in Listing 1.
// Static cast Int32 to double Int32 i = 0; double d; Stopwatch sw; sw.start(); do { staticCastInt32ToDouble(d, i); } while (++i < count); sw.stop(); print("static_cast<double>(Int32)", sw.elapsed()); Any a = 1.0; i = 0; sw.start(); do { unsafeAnyCastAnyToDouble(d, a); } while (++i < count); sw.stop(); print("UnsafeAnyCast<double>(Int32)", sw.elapsed()); // Conversion Int32 to double i = 0; sw.start(); do { lexicalCastInt32ToDouble(d, i); } while (++i < count); sw.stop(); print("boost::lexical_cast<double>(Int32)", sw.elapsed()); DynamicAny da = 1; i = 0; sw.restart(); do { convertInt32ToDouble(d, da); } while (++i < count); sw.stop(); print("DynamicAny<Int32>::convert<double>()", sw.elapsed()); i = 0; sw.restart(); do { assignInt32ToDouble(d, da); } while (++i < count); sw.stop(); print("operator=(double, DynamicAny<Int32>)", sw.elapsed()); // Conversion signed Int32 to UInt16 // ... // Conversion string to double // ... // Extraction double a = 1.0; i = 0; sw.restart(); do { anyCastRefDouble(d, a); } while (++i < count); sw.stop(); i = 0; sw.restart(); do { anyCastPtrDouble(d, a); } while (++i < count); sw.stop(); da = 1.0; i = 0; sw.restart(); do { extractDouble(d, da); } while (++i < count); sw.stop(); // Extraction string // void staticCastInt32ToDouble(double& d, int i) { d = static_cast<double>(i); } void unsafeAnyCastAnyToDouble(double& d, Any& a) { d = *UnsafeAnyCast<double>(&a); } void lexicalCastInt32ToDouble(double& d, int i) { d = boost::lexical_cast<double>(i); } void convertInt32ToDouble(double& d, DynamicAny& da) { d = da.convert<double>(); } void assignInt32ToDouble(double& d, DynamicAny& da) { d = da; } void lexicalCastInt32toUInt16(UInt16& us, Int32 j) { us = boost::lexical_cast<UInt16>(j); } void convertInt32toUInt16(UInt16& us, DynamicAny& da) { us = da.convert<UInt16>(); } void assignInt32toUInt16(UInt16& us, DynamicAny& da) { us = da; } void lexicalCastStringToDouble(double& d, std::string& s) { d = boost::lexical_cast<double>(s); } void convertStringToDouble(double& d, DynamicAny& ds) { d = ds.convert<double>(); } void assignStringToDouble(double& d, DynamicAny& ds) { d = ds; } void anyCastRefDouble(double& d, Any& a) { d = RefAnyCast<double>(a); } void anyCastPtrDouble(double& d, Any& a) { d = *AnyCast<double>(&a); } void extractDouble(double& d, DynamicAny& da) { d = da.extract<double>(); } |
Listing 1 |
Tests have been executed on different platforms, with results shown in Table 1 as well as Figure 1a and 1b (Windows) and 2a and 2b (Linux) 1 . It is worth mentioning that the relative performance comparison between different implementations of comparable functionalities is what we were after here, not the absolute values comparison between the two sets of test results (i.e. the two platforms). To get measurable results, the tested code must be called multiple times. Since compiler optimizations undoubtedly count in the real world, it was desirable to obtain the results reflecting the optimization benefits. At the same time, loop optimization could cause results to be misleading. In order to reconcile the opposing forces, executables were compiled at a reasonable level of optimization 2 , with actual conversion code placed in functions (see Listing 2.) located in a separate compilation unit. This arrangement ensured that all tests incur the same function call penalty, thus preserving the performance ratios, while benefiting from the tested functionality optimization improvements. As the test results demonstrate, DynamicAny performs significantly better than competition in conversion and approximately the same in extraction scenarios. Tests have additionally been compiled and executed on Solaris with Sun CC compiler yielding similar results. For conciseness purposes, those results are not included here but can be obtained from [ DynamicAny ].
void anyCastPtrString(std::string& s, Any& as) { s = *AnyCast<std::string>(&as); } void extractString(std::string& s, DynamicAny& ds) { s = ds.extract<std::string>(); } |
Listing 2 |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Table 1 |
Figure 1 |
Figure 2 |
It is important to mention that the performance results were obtained using the most recent development snapshot from the POCO source code repository [ POCO ]. The DynamicAny::extract<>() code used for performance testing purposes performs approximately two times faster than the code from the last release (1.3.2) 3 . The improvement was achieved by substituting the dynamic_cast with typeid() check in conjunction with static_cast . Additionally, the performance of AnyCast<>() and RefAnyCast<>() has been improved in the development code base through inlining.
Dynamic typing in C++ is a niche functionality with limited application domain and certainly not aimed for use in code on the high-end of the performance requirements spectrum. Nevertheless, we felt that performance is a relevant concern for DynamicAny . Given the database querying scenario mentioned in the first installment of the article, it is easy to envision circumstances where code performs acceptably with small data sets but performance significantly deteriorates as data sets grow. As demonstrated in performance tests, in such circumstances milliseconds rapidly accumulate into seconds or even minutes. The attention given to performance definitely yields a nice return on investment in such scenarios and provides a wider scale range available for comfortable type-agnostic coding.
Size
So far, it all went nice and well for DynamicAny . It provides more features than boost::any with no performance penalty for the overlapping ones. The conversions are significantly faster than boost::lexical_cast . However, when it comes to software, size definitely matters and the moment of truth is inevitably coming. What exactly is the memory overhead of this luxury and how much memory does this class hierarchy consume? Holding only a pointer, the size of DynamicAny on a 32-bit architecture is exactly the same as the size of boost::any (or integer , for that matter) - four bytes. Where DynamicAny pays its price is the code size. There is a significant amount of code doing the heavy lifting behind the scenes and it clearly shows in the size. See Listing 3a for size test source code, Listing 3b for binary sizes and Listing 3c for source code line counts. The results were obtained by compiling non-debug, statically-linked code and running SLOCCount tool [ Wheeler ] on relevant source code files.
// AnySize.cpp static Poco::Any a = 1; static int ai = *Poco::AnyCast<int>(&a); // DynamicAnySizeExtract.cpp static Poco::DynamicAny da = 1; int dai = da.extract<int>(); // DynamicAnySizeConvert.cpp static Poco::DynamicAny dac = 1; static std::string das = dac; // lexical_cast_size.cpp static int lci = 1; static std::string lcis = boost::lexical_cast<std::string>(lci); |
Listing 3a |
Binary sizes: Linux ----- 5160 AnySize.o 23668 DynamicAnySizeExtract.o 25152 DynamicAnySizeConvert.o 9600 lexical_cast_size.o Windows ------- 26,924 AnySize.obj 96,028 DynamicAnySizeExtract.obj 103,943 DynamicAnySizeConvert.obj 84,217 lexical_cast_size.obj |
Listing 3b |
Lines of code: Any 145 DynamicAny* 3,588 lexical_cast 971 |
Listing 3c |
Implementation internals
Based on the information provided in the first installment and the tests results, DynamicAny clearly champions convenience and performance in an optimal way. How was this winning combination achieved? Let's peek into DynamicAny 's internal implementation. At the heart of DynamicAny is the value holder class with virtual conversion function for each supported type. Conversions between numeric types are performed by implementation specializations in following manner:
- implicitly between 'sibling' types for widening conversions
- through static_cast for narrowing and signedness conversions (after series of thorough signedness and numeric limits checks)
Conversions between numeric and string values are performed by means of Poco::NumberFormatter and Poco::NumberParser classes. These classes perform the conversion by means of the sprintf() and sscanf() standard C library functions. The pair of otherwise much dreaded functions is used in a controlled and safe way with target buffers properly sized, so the security problems usually associated with those functions are not a concern [ Seacord06 ]. The performance benefits are obvious from the comparison of the test results with boost::lexical_cast equivalent functionality.
A portion of conversion code for signed 16-bit integer specialization is shown in Listing 4.
template <> class DynamicAnyHolderImpl<Int16>: public DynamicAnyHolder { public: // ... // implicit widening conversion void convert(Int32& val) const { val = _val; } // safe narrowing conversion void convert(Int8& val) const { convertToSmaller(_val, val); } // safe signed/unsigned conversion void convert(UInt64& val) const { convertSignedToUnsigned(_val, val); } // static_cast based conversion void convert(float& val) const { val = static_cast<float>(_val); } // conversion to std::string void convert(std::string& val) const { val = NumberFormatter::format(_val); } // ... }; |
Listing 4 |
Conclusion
During the development of POCO Data library, we wanted to provide a convenient RecordSet class capable of internally storing results and providing values without having programmer worrying about the exact data types returned and the column order thereof. The objectives were to achieve optimal dynamic coding convenience within limits of standard C++ while retaining as much efficiency as possible for a dynamic typing scenario. Additionally, conversion safety and data loss prevention were addressed as well. Through DynamicAny class hierarchy we were able to achieve the objectives. Of course, this work was possible thanks to a solid foundation being laid down by our predecessors. DynamicAny is built on boost::any foundation as well as crucially important C++ features such as C language compatibility, operator overloading and free-standing functions as interface extensions.
Readers curious about implementation and usage details are invited to download POCO from the links supplied at the end of this article. POCO is distributed under Boost license. The community features weblog, forum, mailing list and a friendly attitude toward newcomers. General interest inquiries, bug reports, patches, feature requests as well as code contributions are encouraged and very much appreciated.
Acknowledgements
Kevlin Henney is the originator of the idea and author of boost::any class. Kevlin has provided valuable comments on the article.
Peter Schojer has ported boost::any to POCO, implemented major portions of DynamicAny and provided valuable comments on the article.
Günter Obiltschnig has written majority of the POCO framework. Günter has provided valuable comments on the article as well as advice for test code.
Laszlo Keresztfalvi has provided valuable development and testing feedback, sample usage code as well as valuable comments on the article.
References
[ DynamicAny] Article code and test results archive - http://appinf.us/poco/download/DynamicAny/DynamicAnyArticle.zip
[ POCO] C++ Portable Components development repository - http://poco.svn.sourceforge.net/viewvc/poco/
[ Seacord] Robert C. Seacord. Secure Coding in C and C++ , Addison-Wesley, 2006
[ Wheeler] David A. Wheeler. 'SLOCCount' - http://www.dwheeler.com/sloccount/
Further reading
Bjarne Stroustrup. The C++ Programming Language , Addison-Wesley, 1997
Herb Sutter. 'Modern C++ Libraries', Proceedings, SD West 2007
Kevlin Henney. 'Valued Conversions', C++ Report , July-August 2000
Boost libraries - http://www.boost.org
C++ Portable Components - http://poco.sourceforge.net
Article code repository - http://poco.svn.sourceforge.net/viewvc/poco/poco/articles/DynamicAny/
1 Black bars represent DynamicAny, grey bars represent
Any
/
lexical_cast
results; shorter bar means better performance.
2 /02 for MSVC++, -02 for G++
3 Release information accurate at the time of writing the article.