{"id":542,"date":"2016-02-25T22:48:37","date_gmt":"2016-02-25T21:48:37","guid":{"rendered":"http:\/\/www.martyndavis.com\/?p=542"},"modified":"2016-03-21T22:32:57","modified_gmt":"2016-03-21T21:32:57","slug":"big-o-theory-vs-practice","status":"publish","type":"post","link":"https:\/\/www.martyndavis.com\/?p=542","title":{"rendered":"Big O &#8211; Theory vs Practice"},"content":{"rendered":"<p>I saw a question online recently which basically asked whether big-O notation always holds true in practice. For example, in theory, searching through a vector for an item should be O(n) whereas searching a map should be O(log n). Right?<\/p>\n<p>Let&#8217;s imagine we create a vector with 100,000 random integers in it. We then sort the vector, and run two different searches on it. The first search starts at the beginning, compares the element against the value we&#8217;re searching for, and simply keeps incrementing until we find it. A naive search you might say. Without resorting to std:: algorithms, how might you improve it?<\/p>\n<p>A worthy idea might be a recursive binary, or bisection, search. We might, for example, make 15 bisections to find a value in the upper quadrant of our range, compared to, say 75,000 comparisons in the brute force approach.<\/p>\n<p>That&#8217;s fairly safe to say that the binary search is going to be much faster, right?<\/p>\n<p>Not necessarily. See the results below:<\/p>\n<pre>100000 random numbers generated, inserted, and sorted in a vector:\r\n\u00a00.018477s wall, 0.010000s user + 0.000000s system = 0.010000s CPU (54.1%)\r\n\r\nLinear search iterations: = 75086, time taken:\r\n\u00a00.000734s wall, 0.000000s user + 0.000000s system = 0.000000s CPU (n\/a%)\r\n\r\nBinary search iterations: 17, time taken:\r\n\u00a00.001327s wall, 0.000000s user + 0.000000s system = 0.000000s CPU (n\/a%)<\/pre>\n<p>Are you surprised to see that the binary search took (very) slightly <strong>longer<\/strong> than the brute force approach?<\/p>\n<p>Welcome to the world of modern processors. What&#8217;s going on there is a healthy dose of <strong>branch prediction<\/strong> and plenty of valid <strong>processor cache hits<\/strong> which actually makes the brute force approach viable.<\/p>\n<p>I guess the moral of this story is, as always, don&#8217;t optimise prematurely &#8211; because you might actually be making things worse! Always profile your hot spots and work empirically rather than on what you think you know about algorithm efficiency.<\/p>\n<p>The code used here is reproduced below &#8211; it should compile on VS 2015, clang and g++ without issue. You&#8217;ll need Boost for the timers. You may see wildly different timings depending on optimisation levels and other factors :)<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\n#include &lt;iostream&gt;\r\n#include &lt;vector&gt;\r\n#include &lt;random&gt;\r\n#include &lt;algorithm&gt;\r\n#include &lt;string&gt;\r\n\r\n#include &quot;boost\/timer\/timer.hpp&quot;\r\n\r\nconst int MAX_VAL = 100'000;\r\n\r\ntemplate &lt;typename T&gt;\r\nT LinearSearch(typename std::vector&lt;T&gt;::iterator begin,\r\n\u00a0\u00a0\u00a0 typename std::vector&lt;T&gt;::iterator end, T value)\r\n{\r\n\u00a0\u00a0\u00a0 \/\/ While not ostensibly efficient, branch prediction and CPU cache for the\r\n\u00a0\u00a0\u00a0 \/\/ contiguous vector data should make this go like greased lightning :)\r\n\u00a0\u00a0\u00a0 size_t counter{0};\r\n\u00a0\u00a0\u00a0 while (begin &lt; end)\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 if (*begin &gt;= value)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::cout &lt;&lt; &quot;\\nLinear search iterations: = &quot; &lt;&lt; counter;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 return *begin;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ++begin;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ++counter;\r\n\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0 return 0;\r\n}\r\n\r\ntemplate &lt;typename T&gt;\r\nT BinarySearch(typename std::vector&lt;T&gt;::iterator begin,\r\n\u00a0\u00a0\u00a0 typename std::vector&lt;T&gt;::iterator end, T value)\r\n{\r\n\u00a0\u00a0\u00a0 \/\/ The return value is not exact... just interested in getting close to the\r\n\u00a0\u00a0\u00a0 \/\/ value.\r\n\u00a0\u00a0\u00a0 static size_t counter{0};\r\n\u00a0\u00a0\u00a0 size_t mid = (end - begin) \/ 2;\r\n\u00a0\u00a0\u00a0 ++counter;\r\n\u00a0\u00a0\u00a0 if (begin &gt;= end)\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::cout &lt;&lt; &quot;\\nBinary search iterations: &quot; &lt;&lt; counter;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 return *begin;\r\n\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0 if (*(begin + mid) &lt; value)\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 return BinarySearch(begin + mid + 1, end, value);\r\n\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0 else if (*(begin + mid) &gt; value)\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 return BinarySearch(begin, begin + mid - 1, value);\r\n\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0 else\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::cout &lt;&lt; &quot;\\nBinary search iterations: &quot; &lt;&lt; counter;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 return value;\r\n\u00a0\u00a0\u00a0 }\r\n}\r\n\r\nint main()\r\n{\r\n\u00a0\u00a0\u00a0 \/\/ Fill a vector with MAX_VAL random numbers between 0 - MAX_VAL\r\n\u00a0\u00a0\u00a0 std::vector&lt;unsigned int&gt; vec;\r\n\u00a0\u00a0\u00a0 vec.reserve(MAX_VAL);\r\n\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 boost::timer::auto_cpu_timer tm;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::random_device randomDevice;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::default_random_engine randomEngine(randomDevice());\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::uniform_int_distribution&lt;unsigned int&gt; uniform_dist(0, MAX_VAL);\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 for (int n = 0; n &lt; MAX_VAL; ++n)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 vec.emplace_back(uniform_dist(randomEngine));\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ Sort the vector\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::sort(vec.begin(), vec.end());\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::cout &lt;&lt; MAX_VAL\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 &lt;&lt; &quot; random numbers generated, inserted, and sorted&quot;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 &quot; in a vector:&quot; &lt;&lt; std::endl;\r\n\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 boost::timer::auto_cpu_timer tm2;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 LinearSearch(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 vec.begin(), vec.end(), static_cast&lt;unsigned int&gt;(MAX_VAL * 0.75));\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::cout &lt;&lt; &quot;, time taken:&quot; &lt;&lt; std::endl;\r\n\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0 {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 boost::timer::auto_cpu_timer tm2;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 BinarySearch(vec.begin(), vec.end() - 1,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 static_cast&lt;unsigned int&gt;(MAX_VAL * 0.75));\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 std::cout &lt;&lt; &quot;, time taken:&quot; &lt;&lt; std::endl;\r\n\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0 return 0;\r\n}\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>I saw a question online recently which basically asked whether big-O notation always holds true in practice. For example, in theory, searching through a vector for an item should be O(n) whereas searching a map should be O(log n). Right? Let&#8217;s imagine we create a vector with 100,000 random integers in it. We then sort &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.martyndavis.com\/?p=542\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Big O &#8211; Theory vs Practice&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17,1],"tags":[],"_links":{"self":[{"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=\/wp\/v2\/posts\/542"}],"collection":[{"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=542"}],"version-history":[{"count":11,"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=\/wp\/v2\/posts\/542\/revisions"}],"predecessor-version":[{"id":560,"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=\/wp\/v2\/posts\/542\/revisions\/560"}],"wp:attachment":[{"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=542"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=542"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.martyndavis.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=542"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}