<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Borys Minaiev</title><generator>teletype.in</generator><description><![CDATA[Subscribe to my blog on Telegram!

https://t.me/bminaiev_blog]]></description><image><url>https://img1.teletype.in/files/89/3d/893d6bde-2f5d-4925-8b26-cafb2b0172db.png</url><title>Borys Minaiev</title><link>https://teletype.in/@bminaiev</link></image><link>https://teletype.in/@bminaiev?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/bminaiev?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/bminaiev?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Fri, 10 Apr 2026 19:52:51 GMT</pubDate><lastBuildDate>Fri, 10 Apr 2026 19:52:51 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@bminaiev/jigsaw-puzzle-solver</guid><link>https://teletype.in/@bminaiev/jigsaw-puzzle-solver?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/jigsaw-puzzle-solver?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>Solving Jigsaw Puzzle with bare Rust</title><pubDate>Wed, 30 Nov 2022 21:45:13 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/fc/43/fc4390d2-1bca-40df-a69a-c74f8c370f60.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/a4/91/a491cde3-1b27-412d-8f95-d4f277d2d894.jpeg"></img>I participate in a lot of programming competitions and usually, if you show good results, organizers send you some prizes. Typically it is just t-shirts, but at some point, you have too many of them, so you had to make a bed cover from them. But sometimes prizes are more interesting. This time Google HashCode organizers sent a jigsaw puzzle if you advanced to the finals. That was cool, so I decided to solve it.]]></description><content:encoded><![CDATA[
  <p id="bcFb">I participate in a lot of programming competitions and usually, if you show good results, organizers send you some prizes. Typically it is just t-shirts, but at some point, you have too many of them, so you had to <a href="https://petr-mitrichev.blogspot.com/2014/01/this-week-in-competitive-programming.html" target="_blank">make a bed cover from them</a>. But sometimes prizes are more interesting. This time <a href="https://codingcompetitions.withgoogle.com/hashcode" target="_blank">Google HashCode</a> organizers sent a jigsaw puzzle if you advanced to the finals. That was cool, so I decided to solve it.</p>
  <p id="x5nn">It went pretty well until I got into trouble. I finished a big part of the puzzle, but all the left pieces were completely white. I divided all of them into groups by their shape, but still for each specific position you usually need to try all pieces from two or three groups. It was insanely hard for me to find each next correct piece, and also didn&#x27;t bring much joy. I found only several new pieces over the next couple of evenings and decided to stop. I was thinking <em>&quot;I am a software engineer! I shouldn&#x27;t solve it by hand! I should write a program, which solves it!&quot;</em>. At that point, I took a picture on my phone and put all the pieces back in the box, so I can use a table for other stuff. </p>
  <figure id="5xQS" class="m_column">
    <img src="https://img3.teletype.in/files/a4/91/a491cde3-1b27-412d-8f95-d4f277d2d894.jpeg" width="3120" />
  </figure>
  <h2 id="0CS8">Several months later...</h2>
  <p id="up7L">The difference between the idea of writing a program for solving a puzzle, and actually writing a program, is quite big, but later I had some free time, so I really decided to do it. Back then I didn&#x27;t actually know how hard it would be and if it was actually possible. But I had a rough plan.</p>
  <ul id="qR39">
    <li id="611k"><strong>Crop/reshape the picture.</strong> We want to be able to check if two pieces fit together, so we need to know their exact shapes. But when you take a picture, far objects are smaller in terms of pixels, so we need to fix it. </li>
    <li id="3lyO"><strong>Detect pieces.</strong> We need to separate pieces from the background (and also from other pieces, if they are connected). </li>
    <li id="BSz1"><strong>Detect borders.</strong> When you fit pieces, you actually don&#x27;t care about the full piece, you only need to fit the borders. </li>
    <li id="N44J"><strong>Detect corners.</strong> This was an optional point of the plan. From a human standpoint, you think about the pieces as something, which has four sides, and you fit one side of the piece with another side of another piece. But from a computer standpoint, it can try all possible fitting positions. </li>
    <li id="GB8a"><strong>Fit pairs of pieces.</strong> For each pair of pieces, for each pair of sides, I wanted to try to fit them, and calculate some score, which shows how well they fit.</li>
    <li id="TSC1"><strong>Find the solution.</strong> If we know how good pieces fit, we can forget about shapes and think just about a graph problem, where for each piece we need to find the best position (and rotation) in a grid to maximize the overall fit function. </li>
    <li id="K96K"><strong>Show the solution.</strong> When positions in a grid are known, we need to come back to pieces with shapes and show them. Need to find the correct position and rotation for each piece, so they all fit together. It is also possible that one piece fits well with all the neighbors in terms of the score function, but there is no way to actually put it on a surface to achieve that score function with all neighbors at the same time.</li>
  </ul>
  <p id="59Ks"><em>Now, when I actually wrote down this plan, I realized how many details there are. But when I thought about it, it didn&#x27;t look that scary.</em></p>
  <p id="JdnY">To make things even more fun, I decided to not use any existing smart libraries for computer vision and other stuff, and just work with an image as with a two-dimensional array of <code>(r, g, b)</code> values.</p>
  <h2 id="LsO3">Reshaping the picture</h2>
  <p id="59Mz">Luckily I had a square table in the photo, so I could use its corners as base points for reshaping. Obviously, there are some programs, which could reshape the picture automatically, but I only started working on this project, had a lot of energy, and decided to implement it myself <em>(don&#x27;t repeat my mistakes)</em>. </p>
  <p id="EKhv">My idea was to make a GUI, which lets you pick four corners of a table with a mouse, and then do reshaping. I wanted to put corners of the table into corners of a new picture, and stretch everything else. It is easy to say &quot;stretch&quot;, but how do you do this in terms of pixels?</p>
  <p id="qTaI">Let&#x27;s say we decided that the new picture will have a size 1000x1000. Then we can connect table corners via segments, split them into 1000 equal parts, and then connect points from opposite sides. Each quadrilateral will correspond to one pixel in a new picture. In the case of a 50x50 picture, it looks like this.</p>
  <figure id="a9KD" class="m_column">
    <img src="https://img3.teletype.in/files/28/88/2888081c-65c7-4eba-bd66-feb0c62f0cbf.jpeg" width="1043" />
  </figure>
  <p id="v8Bx">To calculate a color for each new pixel, we can iterate over all pixels inside the quadrilateral, and take an average. It is a little bit more complicated because some pixels could be partially inside several different quadrilaterals, but we can give them a weight proportional to the area of the intersection. We need to do some computational geometry, but in the end, we get a result, which seems pretty reasonable!</p>
  <figure id="c4HG" class="m_column">
    <img src="https://img1.teletype.in/files/83/36/8336ce99-08da-4f55-856d-e23443e11116.jpeg" width="2822" />
  </figure>
  <h2 id="d5sK">But...</h2>
  <p id="JEyy">This method works pretty well, except for one &quot;but&quot;. It doesn&#x27;t do what we need! From this transformation, we need one property. If we choose 4 points <code>[p1, p2, p3, p4]</code> and the distance (in real life) between <code>p1</code> and <code>p2</code> is the same as between <code>p3</code> and <code>p4</code>, it should be the same in the generated picture. This property holds for corners of the table. But it doesn&#x27;t work for other points.</p>
  <p id="mfST">Funny enough, I didn&#x27;t realize the problem until I implemented all other parts of the algorithm and started testing it on real pieces, trying to fit pairs found by the algorithm, and didn&#x27;t succeed.</p>
  <p id="hx33">The issue comes from the fact that we split segments into 1000 <strong>equal</strong> parts. If we take a segment and look at the midpoint (calculated in terms of pixels), it will not have the same distance to two ends in a physical world. It will be closer to a point, which was closer to a camera when the photo was taken.</p>
  <p id="2KEx">Ok, this method is bad, but how to reshape it correctly?</p>
  <p id="NJsv">The transformation, which we need, is called <strong>homography</strong>, and I found a nice <a href="https://towardsdatascience.com/understanding-homography-a-k-a-perspective-transformation-cacaed5ca17" target="_blank">post</a>, which explains it. OpenCV even has a function <code>getPerspectiveTransform</code> for it. But part of the challenge was to not use such libraries, so I implemented it myself.</p>
  <p id="s46e">Let me briefly explain the idea. We say that when we take a photo, all points are projected to some plane (by drawing a line between it and a camera, and looking where it intersects the plane).  We don&#x27;t know the actual camera location, but we want to estimate it based on the fact that we know the positions of specific points (in our case table corners). After that, we can take other pixels from the photo, and calculate their actual physical location.</p>
  <p id="Fdkq">We can express this transformation as multiplication by some 3x3 matrix with unknown coefficients. One of the coefficients could be explicitly set to <code>1.0</code>,   as we don&#x27;t care about the scaling factor. We have 4 corners and for each of them we can write down 2 linear equations (for <code>x</code> and <code>y</code> coordinates). Overall 8 equations and 8 unknown variables, so we can find a unique solution via <a href="https://en.wikipedia.org/wiki/Gaussian_elimination" target="_blank">Gaussian elimination</a>. I skipped some details, but it should be possible to find good explanations about it on the internet.</p>
  <p id="hQMn">Correctly reshaped picture (do you see the difference?): </p>
  <figure id="Jyko" class="m_column">
    <img src="https://img2.teletype.in/files/13/2c/132cd916-deae-4b8e-a748-df9df39356eb.jpeg" width="2000" />
  </figure>
  <h2 id="zs7l">Pieces detection</h2>
  <p id="MZZh">Great, we reshaped the picture, what is next? We need to separate pieces from the background. Again, there are probably some CV libraries, which can do it out of the box, but we want to do it ourselves. </p>
  <p id="G2pl">The worst thing you can do when approaching problems like this is to start writing something smart and complicated. Usually, it will not work, and you will lose a lot of time. As in all other parts of this post, I suggest thinking about how you&#x27;d approach such a problem yourself before reading my solution.</p>
  <p id="IFsc">As all pieces, I cared about, were white, and the background was dark, I decided if a pixel is part of a piece or not by a simple formula:</p>
  <pre id="jUMf" data-lang="rust">fn is_piece_color(color: Color32) -&gt; bool {
    color.r() + color.g() + color.b() &gt;= 500
}</pre>
  <p id="Jizc">I got this result:</p>
  <figure id="DJhF" class="m_column">
    <img src="https://img1.teletype.in/files/86/f7/86f7b704-f23e-4eb8-900d-8ca1cfef1ff5.jpeg" width="2822" />
  </figure>
  <p id="iIUV">I joined all connected pixels into groups, tweaked a constant a little bit, and got this picture:</p>
  <figure id="5CSk" class="m_column">
    <img src="https://img3.teletype.in/files/aa/a1/aaa1857f-8b24-4a76-8699-f8282be2de99.jpeg" width="2822" />
  </figure>
  <p id="RWWY">Obviously, it is not ideal. Some pieces were merged together and some didn&#x27;t contain all the pixels, but it was good enough to start implementing the next stages of the algorithm and test things.</p>
  <h2 id="0Z9e">Borders detection</h2>
  <p id="WxfE">We want to split a set of pixels from one piece into inner points and the border. It is very easy. We say the pixel is on the border if there is another pixel near it, which doesn&#x27;t belong to this piece. After implementing this check, I got a picture:</p>
  <figure id="fpws" class="m_column">
    <img src="https://img2.teletype.in/files/df/f5/dff57a94-4503-4c6d-847e-cacc62ed9e14.png" width="2682" />
  </figure>
  <p id="GKT2">When we know a set of pixels lying on the border, we need to put them in some reasonable order. Intuitively we just want to go around the piece, and write down all the pixels, but in reality, it was hard to come up with some reasonable algorithm, which does it. We need this order to be able to match different pieces together. If we have it, we can traverse two borders at the same time, and check that pixels are not too far from each other. </p>
  <p id="FFkJ">More formally we want to find such an order of pixels that the distance between each pair of neighboring (in that order) pixels is quite small (e.g. less than 3 pixels). Finding such a permutation for a general graph is NP-hard, but we can somehow use special properties of our graph to simplify the task.</p>
  <p id="E9KC">I implemented some greedy algorithm, which just always went to the closest not visited pixel. And then applied some local optimizations to improve generated permutation. Mostly it found good solutions:</p>
  <figure id="juWX" class="m_column">
    <img src="https://img2.teletype.in/files/90/e1/90e1ad4f-35a9-4990-b94b-f07a0de02b32.png" width="1483" />
  </figure>
  <p id="DIRF">But sometimes something didn&#x27;t go well:</p>
  <figure id="uWeq" class="m_column">
    <img src="https://img1.teletype.in/files/c3/d5/c3d5908e-b7ff-4d1c-8efd-16efb8520e63.png" width="1483" />
  </figure>
  <p id="JXCA">Again, we don&#x27;t need a 100% working solution (at least in the testing stage), we can improve it later if it becomes the bottleneck. Btw, if somebody knows a good algorithm for this task — please let me know :)</p>
  <h2 id="ogDB">Corners detection</h2>
  <p id="Cgyn">Great, we detected a border of a piece, and put all pixels of it in, for example, clockwise order. Now we want to split this border into four parts, where each part corresponds to one side of a piece. I hope intuitively it should be easy to understand what side is, but it is hard (at least for me) to formally define it. </p>
  <p id="EDII">Why do we actually need to detect separate sides? </p>
  <ul id="AatV">
    <li id="RwQD">First, it makes matching easier. When we try to fit one piece with another, we only need to try 4x4=16 possible ways. </li>
    <li id="MsrO">Second, it makes the later graph task more discrete. For example, we can say that we need to put pieces in a grid, and for each piece choose one of 4 possible rotations. After that, we know which pieces are connected and by which sides.</li>
  </ul>
  <p id="3y6d">So, how to detect corners? Again, I encourage you to think about this problem before reading my solution!</p>
  <p id="BwLr">How are corners different from other points of the border? One difference is that if you look at several pixels before the corner, and several pixels after, the direction changes a lot. But it is also true for some other points... Also, it is hard to express &quot;changes a lot&quot; in the algorithm. You will probably need to choose some constants like how many pixels to take before and after, and what angle is big enough. And maybe for different pieces, you will need to use different constants. </p>
  <p id="Q4za">Another algorithm, which I tried worked like this. Let&#x27;s first calculate the center mass of the piece (you can actually see the blue dot in the pictures above). Then for each pixel from the border calculate the distance to the center. Then we say the pixel is probably a corner, if it is further from the center, then pixels nearby. It already works pretty well:</p>
  <figure id="WiFn" class="m_column">
    <img src="https://img4.teletype.in/files/f8/d4/f8d4d6a9-9aba-43c1-99f3-217def49c8fc.png" width="1054" />
  </figure>
  <p id="XS2m">This algorithm detects all corners, but also sometimes some additional points (but not too many of them, which is good). We can try to iterate over all possible fours of detected points and choose a four, which is the best by some metric. We only need to come up with a good metric. Intuitively, corners should form a rectangle, so we can choose the most &quot;rectangular&quot; four points. I did two things:</p>
  <ul id="o8cv">
    <li id="8TLh">For every three consecutive points, I calculated the angle between them and compared it with 90 degrees.</li>
    <li id="q6bf">Divided the longest distance between consecutive points by the shortest distance. The idea is to exclude options, where we have two very close points.</li>
  </ul>
  <p id="gLPU">Then I multiplied two values and found the minimum score. Probably there are better/easier ways of doing this, but I just played with formulas until they worked well on real examples:</p>
  <figure id="BakR" class="m_column">
    <img src="https://img4.teletype.in/files/7a/7b/7a7b3dc3-2234-4a95-9ebe-464398acf524.png" width="1750" />
  </figure>
  <h2 id="KxUw">Fitting borders</h2>
  <p id="slFU">Okay, as we detected the sides of each piece, now it should be pretty straightforward to say if two pieces fit or not. We just put them nearby, iterate over pixels from both sides at the same time, and check that the <code>i</code>-th pixel from one border is near the <code>i</code>-th pixel of the border of another piece. </p>
  <p id="lGJe"><em>Wait, put them nearby? How? </em></p>
  <p id="Lvgb">We need to apply some transformation (rotation and shift) to one piece such that the metric we try to improve (sum of distances for corresponding pixels) is the best. How to find such transformation? We can try to fit the corners of one piece with corners from another piece and hope other points will also fit nicely. </p>
  <p id="HDVx">There was also a problem connected to the fact that we don&#x27;t detect pieces on the photo ideally. The actual border of the piece is not very white on a photo, so we don&#x27;t mark it as part of the piece, and our pieces are smaller than in reality. And when we try to fit two pieces, which should fit in real life, borders don&#x27;t match exactly. I first tried to fix it by applying an additional shift and saying the corresponding pixels should have a distance similar to this shift:</p>
  <figure id="fmqC" class="m_column">
    <img src="https://img3.teletype.in/files/6e/ac/6eac3c3b-7bf1-4d13-af90-5e51d330aa18.png" width="1347" />
  </figure>
  <p id="oVCE">But it didn&#x27;t work very well, because, for some pairs of pieces, which shouldn&#x27;t fit at all, I got good scores, because even after the shift, borders intersected much, and the distance between corresponding pixels was negative but equal to the shift by absolute value (and I didn&#x27;t come up with a way to distinguish good positive distance and very bad negative, as I only knew the absolute value of it). </p>
  <p id="VyKe">A good fix, which helped, was to just enlarge each piece by two pixels, and then try to match borders exactly, without a shift.</p>
  <p id="grvk">Back to the trick, where we chose needed transformation based on the corners. Sometimes we don&#x27;t detect corners precisely (maybe off by a couple of pixels). But when we use a slightly incorrectly detected corner, it changes the transformation quite a bit, and we receive not-so-good results:</p>
  <figure id="dijM" class="m_column">
    <img src="https://img2.teletype.in/files/52/3b/523bda39-baf9-4304-a981-eac82b741dee.png" width="1045" />
  </figure>
  <p id="roTt">Obviously, we can rotate the bottom piece a little bit to fit more nicely, but how to say it to the algorithm?</p>
  <p id="3UdZ">I estimated the transformation by corners first and then tried to change it a little bit with local optimizations. For example, we can calculate the score, shift the piece a little bit in a random direction, and check if the score improved. If not, we shift the piece back, but if it improves, we try to do it again. And we can also try to rotate the piece in the same way. After applying this technique I got a good result:</p>
  <figure id="vbVv" class="m_column">
    <img src="https://img2.teletype.in/files/57/75/57759b3b-f45d-4dfb-a0e1-071157d8c9f3.png" width="1142" />
  </figure>
  <h2 id="I0ej">The last part</h2>
  <p id="iiCN"><em>Great, we are almost there! Congratulations if you read till this point!</em></p>
  <p id="7LJV">If for each pair of pieces we can calculate how well they fit, we can build a graph on it. And apply some algorithms to it to find the best possible overall fitting. I didn&#x27;t know a good algorithm, which solves it perfectly, so I wrote a very easy greedy approach. Just try to fit pieces with the best possible scores, until you connect all of them into one figure.</p>
  <p id="egCo">Obviously, it is not perfect, but after all the work done before that moment, I wanted to see some real result, even if it will not give the absolutely correct result. And what do you think I got?</p>
  <figure id="YHRV" class="m_column">
    <img src="https://img2.teletype.in/files/59/ce/59ce9d7c-9e3f-4cd9-9265-14c504fe161a.png" width="2046" />
  </figure>
  <p id="RF8q">Well, this looks very incorrect! But it also looks a little bit promising. You can see that some pairs of pieces actually fit pretty nicely.</p>
  <p id="Gfrv">Anyway, this post is already pretty long, and there is a lot more to cover in this story, so I decided to split it into two parts. <strong>See you in part 2!</strong></p>
  <p id="WoQz">If you want some spoilers, you can check the GitHub repository with the source code and images: <a href="https://github.com/BorysMinaiev/jigsaw-puzzle-solver" target="_blank">https://github.com/BorysMinaiev/jigsaw-puzzle-solver</a>  </p>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="3Je2">If you know Russian and liked this post, consider subscribing to my Telegram channel: <a href="https://t.me/bminaiev_blog" target="_blank">https://t.me/bminaiev_blog</a></p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/online-point-location</guid><link>https://teletype.in/@bminaiev/online-point-location?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/online-point-location?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>Online Point Location Algorithm</title><pubDate>Mon, 10 Oct 2022 14:05:46 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/01/7b/017b2c27-15f7-4644-823b-e71c57a183eb.png"></media:content><tt:hashtag>cp</tt:hashtag><tt:hashtag>geometry</tt:hashtag><description><![CDATA[<img src="https://img4.teletype.in/files/34/3d/343dd898-4925-4ddd-8f51-4e083feb7480.gif"></img>Round 3 of Meta Hacker Cup 2022 finished a couple of days ago. Today I'll talk about the hardest problem from that round, which was solved only by 3 people. The problem is quite classical, so basically after reading the problem statement, experienced competitive programmers already know what they need to write, but the amount of the code is pretty big, and almost nobody likes geometry problems, so this probably explains why there are so few accepted solutions. ]]></description><content:encoded><![CDATA[
  <p id="EGhM">Round 3 of Meta Hacker Cup 2022 finished a couple of days ago. Today I&#x27;ll talk about the hardest <a href="https://www.facebook.com/codingcompetitions/hacker-cup/2022/round-3/problems/E2" target="_blank">problem</a> from that round, which was solved only by 3 people. The problem is quite classical, so basically after reading the problem statement, experienced competitive programmers already know what they need to write, but the amount of the code is pretty big, and almost nobody likes geometry problems, so this probably explains why there are so few accepted solutions. </p>
  <figure id="W4pl" class="m_original" data-caption-align="center">
    <img src="https://img4.teletype.in/files/34/3d/343dd898-4925-4ddd-8f51-4e083feb7480.gif" width="600" />
    <figcaption>Picture from the initial problem statement</figcaption>
  </figure>
  <p id="Ej81">Let me describe the main part of the problem. You are given <code>n</code> polygons, which don&#x27;t intersect (and even touch) with each other, but polygons could be fully enclosed in another. You need to answer <code>q</code> queries one by one. <strong>Each query is one point, and you need to find the smallest (by area) polygon (out of the given in the initial input), which contains this point.</strong> The total number of vertices in all polygons is around one million, and the total number of queries is also around one million. So you can&#x27;t just check all pairs of queries and polygons. </p>
  <p id="XPyp">The classical algorithm for this task is doing a sweep-line by <code>X</code> coordinate and maintaining a set of all edges, which contains the current <code>X</code> coordinate. The set itself should be sorted by <code>Y</code> coordinate. You also need to store all the revisions of this set, so when you receive a query <code>(x, y)</code>, you can find a revision for a specific <code>x</code>, and find a lower bound for <code>y</code> in the set. Because you want to efficiently store revisions, you can&#x27;t just use a built-in <code>std::set</code>, you need to use a persistent treap or a similar data structure. You can find the details of this algorithm (a simplified offline version of it) in <a href="https://cp-algorithms.com/geometry/point-location.html" target="_blank">cp-algorithms</a>. But even without reading this article, you probably already understood why there were so few accepted solutions to the problem from Hacker Cup.</p>
  <p id="912Z">I knew this algorithm before the round, but I don&#x27;t like writing treaps during the contests, so I didn&#x27;t manage to implement it in time (thankfully, solving all other problems was enough to advance to the Finals). After the round, I talked to <a href="https://codeforces.com/profile/PavelKunyavskiy" target="_blank">Pavel</a>, and he showed me <strong>another algorithm</strong>, which could be used for this problem. I didn&#x27;t know it before, it has worse time complexity (<code>O(log^2 n)</code> per query, <code>O(n log n)</code> memory), but I think it is easier to implement. I don&#x27;t think it is very well known. The only place, I could find, where it is mentioned, is in this <a href="https://cs.brown.edu/courses/cs252/misc/resources/lectures/pdf/notes03.pdf" target="_blank">paper</a>, so I decided to share it here.</p>
  <h2 id="sLiF">Main idea</h2>
  <p id="2ivA">Note. We say our polygons don&#x27;t include the boundary, so if the query point exactly lies on the boundary of some polygon, we return the parent of that polygon. It is possible to include boundaries, but it requires some carefulness. </p>
  <p id="2hnp">So we have a point, and we want to find in which polygon it is located. Let&#x27;s draw a ray from a point to the left, and find the first edge, which it intersects. There could be two cases:</p>
  <ol id="3GDb">
    <li id="dom8">Our point is inside the polygon, which contains this edge.</li>
    <li id="7Rri">Our point is just outside of the polygon, which contains this edge. In this case, our point is inside the &quot;parent&quot; polygon of the one, which we just found.</li>
  </ol>
  <figure id="21ph" class="m_column">
    <img src="https://img2.teletype.in/files/97/8d/978dcc13-90d6-4765-9fde-b810fc6793b0.png" width="3000" />
  </figure>
  <p id="tP4l">How to determine what is our case? Let&#x27;s say we stored the vertices of our polygons in the counter-clockwise order. Then if the edge goes from top to bottom, the point is inside the polygon. If from bottom to top — outside of it. </p>
  <p id="YTz5">We ignore edges, which have the same <code>y</code> coordinate of ends.</p>
  <p id="2VNf">How to efficiently find the first edge to the left of the point? Let&#x27;s first discuss how to efficiently find all edges, which intersect line <code>y=C</code>. We can maintain a segment tree by <code>Y</code> coordinate. Each node of the segment tree corresponds to some segment <code>min_y..max_y</code> of <code>y</code> coordinates. In the node, we store a vector of all segments, which fully covers this range of y coordinates. Each edge is added to <code>O(log n)</code> nodes, so overall <code>O(n log n)</code> memory is used.</p>
  <figure id="RlLD" class="m_column" data-caption-align="center">
    <img src="https://img2.teletype.in/files/53/19/5319e9d0-d58c-4ee9-980f-c4a886c03a86.png" width="3000" />
    <figcaption>Segment Tree</figcaption>
  </figure>
  <p id="GY92">Here is a basic type for storing edges.</p>
  <pre id="LBeB" data-lang="rust">#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Segment {
    fr: Point,
    to: Point,
    polygon_id: usize,
}

impl Segment {
    pub fn get_lower_higher(&amp;self) -&gt; (Point, Point) {
        if self.fr.y &lt; self.to.y {
            (self.fr, self.to)
        } else {
            (self.to, self.fr)
        }
    }
}</pre>
  <p id="Gv3k">And let&#x27;s start implementing the main <code>PointLocation</code> structure. Points <code>y</code> coordinates could be arbitrarily large, so we need to compress coordinates (and store them in the <code>all_y</code> field).</p>
  <pre id="q9mJ" data-lang="rust">pub struct PointLocation {
    all_y: Vec&lt;i64&gt;,
    tree_nodes: Vec&lt;Vec&lt;Segment&gt;&gt;,
    ...
}

impl PointLocation {
    // vertices should be specified in ccw order
    pub fn new(polygons: &amp;[Vec&lt;Point&gt;]) -&gt; Self {
        let mut all_y: Vec&lt;i64&gt; = polygons
            .iter()
            .flat_map(|poly| poly.iter().map(|p| p.y))
            .collect();
        all_y.sort();
        all_y.dedup();
        let tree_nodes_cnt = all_y.len().next_power_of_two() * 2;
        let mut res = Self {
            all_y,
            tree_nodes: vec![vec![]; tree_nodes_cnt],
            ...
        };
        for (polygon_id, polygon) in polygons.iter().enumerate() {
            for i in 0..polygon.len() {
                let segment = Segment {
                    fr: polygon[i],
                    to: polygon[if i + 1 == polygon.len() { 0 } else { i + 1 }],
                    polygon_id,
                };
                res.add_segment(0, 0, res.all_y.len() - 1, &amp;segment);
            }
        }
        ...
        res
    }

    fn add_segment(&amp;mut self, tree_v: usize, l: usize, r: usize, segment: &amp;Segment) {
        let min_y = self.all_y[l];
        let max_y = self.all_y[r];
        let (lower, higher) = segment.get_lower_higher();
        if lower.y &lt;= min_y &amp;&amp; higher.y &gt;= max_y {
            self.tree_nodes[tree_v].push(segment.clone());
        } else if lower.y &gt;= max_y || higher.y &lt;= min_y {
            return;
        } else {
            let m = (l + r) &gt;&gt; 1;
            self.add_segment(tree_v * 2 + 1, l, m, segment);
            self.add_segment(tree_v * 2 + 2, m, r, segment);
        }
    }
}</pre>
  <p id="LNWS">To iterate over all edges, which intersect some <code>y=C</code>, we need to just recursively go down by segment tree as we usually do, and list all edges in each node, which we visit.</p>
  <h2 id="vE6L">Comparator</h2>
  <p id="f7ir">Instead of iterating over all edges, which intersect <code>y=C</code>, we want to efficiently find the closest one to the left. We can sort all edges in each node of the segment tree by the <code>X</code> coordinate, and then do a binary search to find the closest one. If we do that, the overall complexity for each query will be <code>O(log^2 n)</code>, as we need to run a binary search on <code>O(log n)</code> segment tree nodes. How to do a binary search? We just want to know, if the point is to the left or to the right of the edge. This is done by a simple vector (cross) product:</p>
  <pre id="NK9l" data-lang="rust">impl Segment {
    pub fn cmp_p(&amp;self, p: Point) -&gt; Ordering {
        let (lower, higher) = self.get_lower_higher();
        if p.y &lt; lower.y || p.y &gt; higher.y {
            return Ordering::Equal;
        }
        Point::vect_mul(&amp;lower, &amp;higher, &amp;p).cmp(&amp;0)
    }
}</pre>
  <p id="28ei">There is a special case for a point with too big or too small <code>y</code> coordinate. During the binary search, it should never happen as we only do it over the edges, which intersect our <code>y</code> coordinate. But it will help us later.</p>
  <p id="Gg71">To do a binary search, we should sort all edges inside each node of the segment tree. But what comparator should we use? They should be sorted in the order of increasing <code>X</code> coordinates, but it is a little bit tricky. We have an invariant that each edge stored in the node covers at least segment <code>y_min..y_max</code> of that node. For each edge, we can compute an <code>x</code> coordinate of an intersection of this edge and <code>y_min</code> and then sort by that <code>x</code>.</p>
  <figure id="Seo6" class="m_column" data-caption-align="center">
    <img src="https://img2.teletype.in/files/58/0f/580f18c0-5142-4e36-b80f-b98b507ea9ce.png" width="3000" />
    <figcaption>Comparing segments is not as easy as you can think...</figcaption>
  </figure>
  <p id="DBj6">But computing this x coordinate requires floating point arithmetic, which should be avoided whenever possible. So instead we can write comparator differently. When we want to compare segment <code>(p1, p2)</code> with <code>(p3, p4)</code>, it is always possible to find one point of <code>[p1, p2, p3, p4]</code>, such that the <code>y</code> coordinate of that point is covered by another segment. When we found this point, we can compare it with another segment using the same comparator as in the binary search.</p>
  <p id="bFIn">It is possible to prove that this comparator and floating point one returns the same result.</p>
  <p id="bXCC">The final version of the comparator looks like this:</p>
  <pre id="nLh7" data-lang="rust">impl Ord for Segment {
    fn cmp(&amp;self, other: &amp;Self) -&gt; Ordering {
        self.cmp_p(other.fr)
            .then_with(|| self.cmp_p(other.to))
            .then_with(|| other.cmp_p(self.fr).reverse())
            .then_with(|| other.cmp_p(self.to).reverse())
    }
}</pre>
  <p id="h3VU"><code>then_with</code> tries the next possibility if the previous comparator returned <code>Ordering::Equal</code>. It is the moment when our special case from <code>cmp_p</code> helps. </p>
  <h2 id="z2d1">Point location</h2>
  <p id="1GOE">The only thing left is to implement the searching logic. But it is very similar to regular segment tree implementation. I implemented it without recursion just to speed it up a little bit. </p>
  <pre id="7TXD" data-lang="rust">pub fn locate_point(&amp;self, p: Point) -&gt; Option&lt;usize&gt; {
    let mut segment: Option&lt;Segment&gt; = None;
    let mut tree_v = 0;
    let (mut l, mut r) = (0, self.all_y.len() - 1);
    loop {
        let min_y = self.all_y[l];
        let max_y = self.all_y[r];
        if p.y &lt; min_y || p.y &gt; max_y {
            break;
        }
        if let Some(idx) = binary_search_last_true(0..self.tree_nodes[tree_v].len(), |i| {
            self.tree_nodes[tree_v][i].cmp_p(p) == Ordering::Less
        }) {
            let new_segment = self.tree_nodes[tree_v][idx];
            if segment.is_none() || segment.unwrap().cmp(&amp;new_segment) == Ordering::Less {
                segment = Some(new_segment);
            }
        }
        if l + 1 &lt; r {
            let m = (l + r) &gt;&gt; 1;
            let mid_y = self.all_y[m];
            if p.y &lt; mid_y {
                tree_v = tree_v * 2 + 1;
                r = m;
            } else {
                tree_v = tree_v * 2 + 2;
                l = m;
            }
        } else {
            break;
        }
    }
    segment.and_then(|segment| {
        if segment.fr.y &lt; segment.to.y {
            self.parents[segment.polygon_id]
        } else {
            Some(segment.polygon_id)
        }
    })
}</pre>
  <p id="yjK9">One thing, which I haven&#x27;t covered is <code>self.parents</code>. For each <code>i</code>, it stores the smallest polygon <code>self.parents[i]</code>, which contains polygon <code>i</code>. How do we compute it? It is very easy. We can just run the <code>locate_point</code> function on the leftmost point of that polygon. The only tricky moment is that we need to call it in the correct order to make sure that we already computed <code>parents</code> for all polygons to the left of it. </p>
  <pre id="0W0q" data-lang="rust">impl PointLocation {
    // vertices should be specified in ccw order
    pub fn new(polygons: &amp;[Vec&lt;Point&gt;]) -&gt; Self {
        ...
        for node in res.tree_nodes.iter_mut() {
            node.sort();
        }
        let mut polygons_left_points: Vec&lt;_&gt; = polygons
            .iter()
            .enumerate()
            .map(|(id, poly)| (poly.iter().min().unwrap(), id))
            .collect();
        polygons_left_points.sort();
        for (&amp;p, polygon_id) in polygons_left_points.into_iter() {
            res.parents[polygon_id] = res.locate_point(p);
        }
        res
    }
}</pre>
  <h2 id="NvYq">Conclusion</h2>
  <p id="dCI5">I think this algorithm is easier to implement than a treap-based solution. The only tricky moment is comparators. But they are basically the same for both solutions. Everything else is just a regular segment tree code. The full code for the HackerCup problem could be found <a href="https://github.com/BorysMinaiev/rust-contests/blob/main/archive/2022/10/10.10.2022%20-%20Meta%20Hacker%20Cup%202022%20Round%203/e2_zero_crossings_chapter2.rs" target="_blank">here</a>.</p>
  <p id="BmY1">It has <code>O(n log^2 n)</code> complexity, but in fact, it is pretty fast. My code solves the full dataset of the HackerCup problem under 5s on my laptop even in a single thread. </p>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="BtTx">If you read till this point and know Russian, consider subscribing to my Telegram channel with similar posts: <a href="https://t.me/bminaiev_blog" target="_blank">https://t.me/bminaiev_blog</a></p>
  </section>
  <tt-tags id="pqCi">
    <tt-tag name="cp">#cp</tt-tag>
    <tt-tag name="geometry">#geometry</tt-tag>
  </tt-tags>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/icfpc-2022</guid><link>https://teletype.in/@bminaiev/icfpc-2022?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/icfpc-2022?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>ICFPC 2022</title><pubDate>Wed, 07 Sep 2022 20:21:35 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a8/e0/a8e0f205-78c8-4156-b8c4-de89b2e68926.png"></media:content><tt:hashtag>icfpc</tt:hashtag><tt:hashtag>contests</tt:hashtag><description><![CDATA[<img src="https://img1.teletype.in/files/41/ad/41adac46-8acd-45b0-b2a9-24897fa6b27d.png"></img>For the third year in a row, I participated in the ICFP Contest as a member of RGBTeam with Roman Udovichenko and Gennady Korotkevich. We won it last year and hopefully did a good job this year as well. The final results are not published yet, but we were in the first place when the scoreboard was frozen (2 hours before the end of the contest). Obviously, some teams could have submitted better solutions during the freeze, so the final results could be a little different.]]></description><content:encoded><![CDATA[
  <p id="OmMm">For the third year in a row, I participated in the <a href="https://en.wikipedia.org/wiki/ICFP_Programming_Contest" target="_blank">ICFP Contest</a> as a member of <strong>RGBTeam</strong> with <strong>Roman Udovichenko</strong> and <strong>Gennady Korotkevich</strong>. We won it last year and hopefully did a good job this year as well. The final results are not published yet, but we were in the first place when the scoreboard was frozen (2 hours before the end of the contest). Obviously, some teams could have submitted better solutions during the freeze, so the final results could be a little different.</p>
  <figure id="5uh1" class="m_column" data-caption-align="center">
    <img src="https://img1.teletype.in/files/41/ad/41adac46-8acd-45b0-b2a9-24897fa6b27d.png" width="851" />
    <figcaption>Frozen scoreboard (2 hours before the end)</figcaption>
  </figure>
  <p id="WJDO">One of the differences between the ICFP Contest and other competitions is that problem is prepared by different people every year, so you get a unique experience each time. This year&#x27;s task was quite straightforward (say, compared to two years ago), but still quite interesting and challenging. So thanks a lot to the organizers for preparing it!</p>
  <h2 id="eDfK">Problem</h2>
  <p id="QEs7">The detailed problem statement could be found <a href="https://icfpcontest2022.github.io/specification/" target="_blank">here</a>, but let me briefly describe it. You were given a picture, which you need to draw by doing some operations. Initially, you start from one empty rectangle (actually, that was not true for some of the tasks, but more on that later), and can do some operations:</p>
  <ul id="Sl6O">
    <li id="EQH4">Split a rectangle into two by a line, which is parallel to the <code>X</code> or <code>Y</code> axis.</li>
    <li id="JRn8">Split a rectangle into four by two lines parallel to the <code>X</code> and <code>Y</code> axis. </li>
    <li id="cFX7">Merge two adjacent rectangles. It is only allowed if the merged figure is still a rectangle.</li>
    <li id="xsJp">Fully color one rectangle.</li>
    <li id="2P2Z">Swap two rectangles. It is only allowed if both rectangles have the same size.</li>
  </ul>
  <p id="LfOJ">When you merge or split rectangles, previous rectangles are destroyed and you can&#x27;t refer to them anymore. </p>
  <p id="FdX3">Each type of operation has some cost associated with it. Also, the cost of the operation is <strong>divided</strong> by the area of the rectangle it is applied to (in the case of a merge operation — the area of the biggest rectangle). So coloring the whole canvas is very cheap (because we divide by the big area), and coloring one pixel is very-very expensive. </p>
  <p id="U8dr">This cost function seems controversial, but probably the idea was to force participants to use big rectangles to create pictures, which are still similar to a very detailed picture. </p>
  <p id="Vm6Z">After you have done all operations, the generated picture is compared to a target one pixel by pixel. The difference between two pixels is calculated as the Euclidian distance between RGB values:</p>
  <figure id="Bm1o" class="m_column">
    <iframe src="https://math.embed.fun/embed/pSXvHTEJkG1jFYtPLxw7BZ"></iframe>
  </figure>
  <p id="bWyh">To calculate the final score, you need to sum the cost of all operations and the differences between all pixels. A lower score is better. Here is an example of the target picture and picture produced by some operations with a total score of 17139.</p>
  <figure id="wgz0" class="m_column" data-caption-align="center">
    <img src="https://img2.teletype.in/files/5a/7a/5a7a14d0-bcf7-4d32-8fa3-76d4f3db30c9.png" width="1641" />
    <figcaption>Task 15. Score = 17139</figcaption>
  </figure>
  <p id="UzGL">In total there were 40 different tasks, and the overall score is just a sum of scores for each task. </p>
  <h2 id="HTu1">Initial approach</h2>
  <p id="3lp7">This problem could be solved by <a href="https://www.educative.io/courses/grokking-dynamic-programming-patterns-for-coding-interviews/m2G1pAq0OO0" target="_blank">dynamic programming</a>. For each state <code>(x_left, y_bottom, width, height)</code> we can calculate the optimal scoring function. Transitions are simple:</p>
  <ul id="PqFT">
    <li id="0emG">Fully color this rectangle.</li>
    <li id="vKhc">Split rectangle by line into two. In this case, you need to calculate the sum of scores for two smaller <code>dp</code> states and the cost of the split operation. </li>
  </ul>
  <p id="iWLJ">There are <code>O(N^4)</code> states in this <code>dp</code>, where <code>N</code> is the width/height of the picture (which was 400). When we fully color the rectangle, we need to calculate the sum of distances for all pixels in this rectangle, which can be done in <code>O(N^2)</code>. So naively this algorithm requires <code>O(N^4)</code> memory and does <code>O(N^6)</code> operations, which are quite big numbers for <code>N=400</code>.</p>
  <p id="vXNl">But we can optimize this algorithm by reducing the <code>N</code>. Instead of running it on the initial picture, we can first split the picture into blocks of size 10x10, for each block calculate the average color, and then run the algorithm on the picture of size 40x40. Of course, it will not find the optimal answer, and will only draw rectangles with coordinates, which are multiple of 10, but at least it could finish in a reasonable amount of time.</p>
  <p id="zYO7">And if you are ready to wait, you can use blocks of smaller size instead of 10x10. You can also optimize performance a little bit by not calculating scores for some transitions/states. For example, if you have considered splitting the rectangle into two and know the best possible score, and now trying to check if just coloring the whole rectangle could lead to a better score. You can start iterating through all pixels in this rectangle and calculate the sum of distances with the target picture. But if the sum already exceeds the best score from splitting, you can stop there, and not consider this move. Or you can do some kind of estimations like &quot;consider 10% of random pixels, approximate the score, only fully recalculate if it is good enough&quot;. Such optimizations could speed things up a little bit, but not dramatically (you still can&#x27;t run <code>dp</code> on the whole 400x400 picture).</p>
  <h2 id="1Vlo">Scoring function</h2>
  <p id="7dZt">As I already mentioned the cost of operations is calculated by a little bit strange formula when you pay proportional to the inverse of the rectangle area. Let&#x27;s say we just want to draw a single pixel <code>(x, y)</code> in the middle of the canvas. The Naive way to do this looks like this:</p>
  <figure id="2exF" class="m_column">
    <img src="https://img1.teletype.in/files/88/83/88832d6e-adda-4c91-b3d3-6006a7ce33b7.png" width="2140" />
  </figure>
  <ol id="H194">
    <li id="9lYo">Split the canvas by <code>(x, y)</code>.</li>
    <li id="4EY8">Split the top-right part by <code>(x+1, y+1)</code>.</li>
    <li id="zFRg">Pixel <code>(x, y)</code> is a separate rectangle now, so we can color it.</li>
  </ol>
  <p id="Vxhd">The third operation costs a lot as the area of the colored rectangle is just 1.</p>
  <p id="YL93">Instead, we can have a longer sequence of moves, but which doesn&#x27;t require operations with small rectangles:</p>
  <figure id="XSPe" class="m_column">
    <img src="https://img4.teletype.in/files/b1/1b/b11bc38c-3543-4886-a47f-3ffb8c4f84ec.png" width="1985" />
  </figure>
  <ol id="Uzxm">
    <li id="qj4c">Split by <code>(x, y)</code>.</li>
    <li id="yQAe">Fully color the top-right rectangle.</li>
    <li id="0rKa">Split the top-right rectangle by <code>(x+1)</code>.</li>
    <li id="fxSX">Color the right part of it white.</li>
    <li id="j5fV">Split the left part by <code>(y+1)</code>.</li>
    <li id="kkJm">Color the top part of it white.</li>
  </ol>
  <p id="AWV2">There is a problem with the second approach. If something was already drawn in the top-right corner, it will be cleared by our operations. So we can&#x27;t use it naively to draw all rectangles. But we can always sort all rectangles we want to draw in a way that drawing each new rectangle doesn&#x27;t cause any problems to all the previous ones. </p>
  <p id="NiC6">In this approach, we don&#x27;t even need to do operations 3-6. We can always assume there will be some later rectangle, which will recolor the top-right corner correctly.</p>
  <p id="LIow">To summarize, our algorithm will look like this:</p>
  <ul id="TaDU">
    <li id="naUd">Split the whole picture into rectangles, where each rectangle will be colored into one color.</li>
    <li id="LPKM">Sort rectangles by some magic comparator.</li>
    <li id="AkbD">For each rectangle in order, color it and the area to the top-right of it into one color. This is done by splitting the whole canvas by the left-bottom corner of the rectangle, coloring one part, and merging all parts into one big rectangle back.</li>
  </ul>
  <h2 id="CihL">Local optimizations</h2>
  <p id="9zB3">ICFP Contest is usually 72 hours long, and the first 24 hours are usually called lightning division. There is a special &quot;prize&quot; for being the first on the leaderboard after the first 24 hours, so you want to have some working solution for this moment. </p>
  <p id="0Hw2">After the first ~20 hours, we had a solution similar to what is described above. It used some dynamic programming, and the trick to color rectangles. It gave us a pretty decent score, but still quite far from first place. </p>
  <p id="ldx9">As I already wrote in a <a href="https://teletype.in/@bminaiev/local-optimizations" target="_blank">previous post</a> (sorry, in Russian), local optimizations are a really powerful technique. It is quite common in marathon-style contests, that you generate some reasonable solution first, and then try to iteratively change it a little bit to improve the score. </p>
  <p id="wgpH">To be able to do local optimizations, we need to represent our solution in a way that small changes to the solution don&#x27;t lead to a completely different resulting picture. For example, if we represent the solution as just a sequence of applied operations and try to add/delete some operations, it will not work very well as inserting one operation could completely change the meaning of the next operations.</p>
  <p id="6ElF">Instead, we can represent our solution as just a list of bottom-left corners of rectangles with their colors. This list is sorted in an order in which rectangles are drawn. </p>
  <p id="prPi">We can start by picking a random corner from the list, and moving it by 1 in a random direction. If we get a solution with a better score, we leave a corner in a new place, otherwise, move it back. And trying to do this operation in a loop while the score improves. </p>
  <p id="Lrwu">This alone works pretty well. One of the reasons is explained by how we build our initial solution. We used a dp, which doesn&#x27;t have one-pixel precision, instead, all coordinates are rounded to the closest block size. So if the target picture contains some &quot;border&quot; between two objects, our dp probably guessed this border incorrectly by a couple of pixels. </p>
  <p id="3aE5">We can also try to delete existing corners or add new ones in our local optimizations. But adding new corners doesn&#x27;t work well, because we need to guess a good position for a corner, and the probability of it is pretty low.</p>
  <h2 id="i7rg">Color picking</h2>
  <p id="mRM9">Another optimization that we made before the 24-hour mark is picking a better color for each &quot;color&quot; operation. Initially in dynamic programming, when we want to fully color a rectangle, we just used an average of all target pixels lying in that rectangle. But this is not optimal. </p>
  <p id="8bZo">Let&#x27;s consider a simple case of the rectangle consisting of one pixel with color <code>(255, 0, 0)</code> and 10 pixels with color <code>(0, 0, 0)</code>. For such a case, we can only consider the first coordinate (red) as others are equal to zero. The weighted average of the red component is <code>(255 + 0 * 10) / 11 = 23</code>. And the cost of this rectangle is <code>10 * 23 + 1 * (255 - 23) = 462</code>.</p>
  <p id="cIhP">Instead, we can just use color <code>(0, 0, 0)</code> for this rectangle. And the score for this color will be <code>10 * 0 + 1 * 255 = 255</code>. Which is much better than <code>462</code>!</p>
  <p id="Rkvf">Finding the best color in the general case is actually a known problem called <a href="https://en.wikipedia.org/wiki/Geometric_median" target="_blank">Geometric median</a>.  Wiki suggests there is a <strong>Weiszfeld&#x27;s</strong> <strong>algorithm</strong>, which can be used for solving it. In the end, some of our solutions used it, but we started with a simpler algorithm, which is <strong>Local optimizations</strong> (again). </p>
  <p id="eB91">Let&#x27;s start with a weighted average color (which is a good approximation to a correct answer), and then try to change one of the <code>(red, green, blue)</code> components by one in one of two directions. After changing, we recalculate the cost, and, if it is better than before, continue with a new color. If we tried all 6 possible changes and none of them led to a better answer, we stop. </p>
  <p id="mjFb">This algorithm is very easy to code, but actually works pretty fast and, I believe, always finds the best color. </p>
  <h2 id="3rpC">Local optimization improvements</h2>
  <p id="HRkA">To show how good local optimizations work, let&#x27;s just look at one example. Here is a target picture for test 21:</p>
  <figure id="EhCd" class="m_original" data-caption-align="center">
    <img src="https://img4.teletype.in/files/30/7a/307a0df6-09f0-416c-b397-97ec805bcd59.png" width="400" />
    <figcaption>Target picture</figcaption>
  </figure>
  <p id="NCn7">Here is a solution generated by dynamic programming with <code>block_size = 6</code>. Score = 22087.</p>
  <figure id="DTW5" class="m_original" data-caption-align="center">
    <img src="https://img2.teletype.in/files/9e/e6/9ee6c2f7-3128-4990-9d21-f328a228ac47.png" width="400" />
    <figcaption>Dynamic programming. Score = 22087.</figcaption>
  </figure>
  <p id="4wxZ">And here is the same solution after local optimizations. The score is 14653.</p>
  <figure id="KGle" class="m_original" data-caption-align="center">
    <img src="https://img3.teletype.in/files/6a/7c/6a7c9959-bf33-42b6-9371-d01eb717e650.png" width="400" />
    <figcaption>After local optimizations. Score = 14653.</figcaption>
  </figure>
  <p id="5fXP">Maybe the difference in the generated pictures is not that big, but the difference in scores is really huge! The best answer found for this test during the whole competition is 12567, which is not that far from 14657 found by 10 seconds of local optimizations. </p>
  <p id="AL7e">The results of the lightning division are currently not published yet, but I am pretty confident that we won it by a huge margin (thanks again to local optimizations!). </p>
  <h2 id="Z8Uv">Merging</h2>
  <p id="F45C">After the lightning division finished, there was a small modification to the problem statement, and new tests were added. The difference was that in new tests the initial canvas is not empty. Instead, it was split into squares of different colors. </p>
  <figure id="U0qO" class="m_column" data-caption-align="center">
    <img src="https://img4.teletype.in/files/b7/14/b71446c6-3b84-4fe6-acef-d48eb064f8a6.png" width="1250" />
    <figcaption>Example of a new task (32)</figcaption>
  </figure>
  <p id="m9tB">I am not sure what was the expected way of handling new cases, but when we saw this modification, we almost instantly decided that we just need to merge all squares into the big one, clear it, and draw the picture as we did in previous tests. </p>
  <p id="I1HX">Merging all squares into one seemed like not a very hard task. We can just merge each line separately, and then merge lines together. So we coded this pretty fast and got some scores.</p>
  <p id="UmuG">We also wrote a tool to show the top scores for each test. It looked like this:</p>
  <figure id="NIWJ" class="m_retina">
    <img src="https://img2.teletype.in/files/5f/17/5f177673-2353-4488-89ae-6a988a7dd52a.png" width="842.5" />
  </figure>
  <p id="FGC4">I should also mention that all new tests had target pictures that exactly matched the target pictures of previously known tests. For example, the target picture for test 32, which is shown above, is absolutely the same as for test 9. At some point, we saw a team <strong>CowDay</strong>, which had a score on new tasks, which is much lower than we spent on just merging all squares together (not including an additional cost we pay to actually draw a picture after merging). There were two possible options:</p>
  <ul id="ia9P">
    <li id="9ea1">They do not merge squares, and instead, use them to draw a target picture somehow.</li>
    <li id="4R92">They learned how to pay less for merging.</li>
  </ul>
  <p id="CFVR">We checked the difference between their scores for new tests and corresponding old tests. And for both tests where the initial canvas is split into 20x20 squares, the difference was 19877, which could not be a coincidence. So probably they merged squares more efficiently (we spent 28451 on merging), but how?</p>
  <p id="1dS7">After quite some time of thinking, we realized that when merging squares together, it is sometimes important to use the <strong>&quot;cut&quot;</strong> operation!</p>
  <p id="ueih">As I already mentioned, the cost of operations is proportional to the inverse of the rectangle area. So we want to reduce the number of operations with small rectangles. When we naively merge each line separately, we start each line by merging two small squares, which costs a lot. Instead, we can do something like:</p>
  <ul id="Mc8X">
    <li id="JxIa">Merge the first 7 rows separately.</li>
    <li id="L4NJ">Merge them into one big 7x20 block.</li>
    <li id="ZutO">Split this 7x20 block into 20 blocks of size 7x1.</li>
    <li id="BcCM">For each 7x1 block add 13 more cells, so now we have 20 columns of size 20x1.</li>
    <li id="1uaz">Merge all 20 columns together.</li>
  </ul>
  <p id="YZTE">This is how it works for a 16x16 case.</p>
  <figure id="ZIMx" class="m_original">
    <img src="https://img1.teletype.in/files/c9/06/c9069c6b-38a2-444d-8929-de7d547cff9e.gif" width="401" />
  </figure>
  <p id="IlIK">This way requires more operations than the naive one, but it only uses 7 very costly operations instead of 20. The cost of this way is 19872, which almost matched the difference the <strong>CowDay </strong>team had in their scores, so looked like we found the same way as them. </p>
  <p id="8Kti">After some time we noticed that team <strong>Unagi</strong> does something even more clever with the cost of 17571. </p>
  <p id="h2h3">In the end, we realized we can generalize our solution to use several steps:</p>
  <ul id="Ql5H">
    <li id="c8bO">Build first <code>A</code> rows.</li>
    <li id="6QOO">Build first <code>B</code> columns.</li>
    <li id="ky8k">Build next <code>C</code> rows.</li>
    <li id="oKTy">Build next <code>D</code> columns.</li>
    <li id="8dws">...</li>
  </ul>
  <p id="jfog">Values <code>(A, B, C, D, ...)</code> could be computed with dynamic programming. The state of the dynamic programming is (we built <code>X</code> first rows, and <code>Y</code> first columns, which are represented by two rectangles). There are two ways of representing a state as a union of two rectangles, so we also need to add an additional bit, which indicates it, to the state. And transitions are just adding some amount of new rows or columns. </p>
  <p id="ySAS">Our final method for 16x16 looked like this:</p>
  <figure id="xTE6" class="m_original">
    <img src="https://img3.teletype.in/files/e0/d3/e0d3a9fd-6d3b-4d79-9cd6-e7e7da09d5e3.gif" width="401" />
  </figure>
  <p id="hEQH">Generating this sequence of operations looks like a nightmare, and I am very happy to have teammates who did this instead of me!</p>
  <h2 id="McwC">More optimizations</h2>
  <p id="ff5O">We also tried a number of other optimizations. Some of them improved the score, but not very dramatically. </p>
  <ul id="5oEA">
    <li id="DinQ">We used <a href="https://en.wikipedia.org/wiki/Simulated_annealing" target="_blank">Simulated annealing</a> instead of local optimizations in the end.</li>
    <li id="4eSD">We tried 4 possible rotations of the picture. In our solution, the top-right corner of the picture is very special, and drawing rectangles there cost a lot. So if we rotate a picture such that the top-right corner doesn&#x27;t contain complicated objects, it costs less.</li>
    <li id="FubD">We tried to use the swap operation to move complicated parts of the picture far from the top-right corner. We did this mostly manually and improved the scores on a couple of tests. But based on other teams&#x27; writeups, we underestimated the importance of this operation, and it could be used much more efficiently. </li>
    <li id="uiWU">We had a really nice visualization tool, which also supported some manual modifications of the solutions. </li>
  </ul>
  <h2 id="fzp5">Infrastructure</h2>
  <p id="Ckrj">It is not uncommon in ICFP Contest to use a lot of external computational resources as in most of the algorithms like simulated annealing if you give them more time, they will find a better solution. </p>
  <p id="hobp">But we decided to just use our 3 laptops to run our code. I think it forces you to write smarter algorithms, not rely on big servers :)</p>
  <p id="YALH">In terms of programming languages, I used <strong>Rust</strong> and my teammates used <strong>C++</strong>. We also had some tools written in <strong>Python</strong>.</p>
  <h2 id="EXzA">Final thoughts</h2>
  <p id="UOZa">I enjoyed this year&#x27;s contest. There were a couple of technical issues during the contest, but they all were resolved. The problem was a little bit straightforward (e.g. our solutions didn&#x27;t change much since 24h mark), but still pretty interesting. Thanks a lot to the organizers!</p>
  <p id="1gCe">Looking forward to the ICFPC 2023!</p>
  <tt-tags id="IWl5">
    <tt-tag name="icfpc">#icfpc</tt-tag>
    <tt-tag name="contests">#contests</tt-tag>
  </tt-tags>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/prefetching</guid><link>https://teletype.in/@bminaiev/prefetching?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/prefetching?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>O(N^2). Часть 2: prefetching</title><pubDate>Mon, 22 Aug 2022 14:25:53 GMT</pubDate><tt:hashtag>rust</tt:hashtag><tt:hashtag>simd</tt:hashtag><tt:hashtag>cp</tt:hashtag><tt:hashtag>prefetch</tt:hashtag><description><![CDATA[Продолжаю рассказывать о том, как получать AC с простыми решениями, у которых асимптотика сильно хуже чем хотели авторы задачи. ]]></description><content:encoded><![CDATA[
  <p id="eCrC"><a href="https://teletype.in/@bminaiev/cp-simd-optimizations" target="_blank">Продолжаю</a> рассказывать о том, как получать AC с простыми решениями, у которых асимптотика сильно хуже чем хотели авторы задачи. </p>
  <p id="fdRA">В этот раз речь пойдет о задаче <a href="https://codeforces.com/contest/1718/problem/F" target="_blank">CF814F</a>. Ее формулировка довольно простая. Есть массив <code>a</code> из 10^5 чисел, каждое из которых не больше 2⋅10^4. Нужно ответить на 10^5 запросов <code>(l, r)</code>. Для каждого запроса нужно посчитать количество взаимнопростых чисел от 1 до 10^5 с числом <code>a[l] * a[l + 1] * ... * a[r]</code>.</p>
  <p id="P5NN"><a href="https://codeforces.com/blog/entry/106049" target="_blank">Авторское решение</a> — какая-то корневая со сложной асимптотикой. Мы же будем писать простое решение, которое работает за O(N^2).</p>
  <h2 id="0IIK">Первая идея</h2>
  <p id="DPU2">Для каждого числа <code>x</code> от 1 до 2⋅10^4 посчитаем битовую маску <code>mask[x]</code> длиной 10^5, где <code>i</code>-й бит проставлен, если <code>gcd(x, i) != 1</code>. Тогда чтобы ответить на запрос <code>(l, r)</code> нужно посчитать <code>mask[a[l]] | mask[a[l + 1]] | ... | mask[a[r]]</code>, а потом посчитать количество проставленных бит в полученной маске.</p>
  <p id="WXoc">Ровно в таком виде решение имеет асимптотику O(N^3) и естественно будет работать долго. Но можно добавить divide&amp;conquer и немного оптимизаций для маленьких простых и получить <a href="https://codeforces.com/blog/entry/106049?#comment-945118" target="_blank">AC</a>. Но мы пойдем другим путем.</p>
  <h2 id="W8t3">Вторая идея</h2>
  <p id="SrBS">Идея с масками прикольная, но асимптотика получается слишком большой. Вместо этого можно воспользоваться методом сканирующей прямой. Будем перебирать левую границу запросов <code>l</code> от больших к меньшим и для каждого числа <code>x</code> поддерживать массив <code>first[x]</code> — наименьшая позиция <code>&gt;= l</code> такая, что <code>gcd(x, a[first[x]]) != 1</code>. Тогда чтобы ответить на запрос <code>(l, r)</code> нужно посчитать количество позиций <code>x</code> таких, что <code>first[x] &lt;= r</code>. Это можно сделать простым циклом, который хорошо оптимизируется и работает быстро. </p>
  <p id="e6ys">Осталось только научиться быстро пересчитывать массив <code>first</code>, когда передвигаем указатель <code>l</code>. Все что нужно сделать — посмотреть на <code>mask[a[l]]</code> и для всех позиций <code>x</code>, где проставлен бит, обновить <code>first[x] = l</code>.</p>
  <h2 id="w1DT">mask[x]</h2>
  <p id="bKoz">Давайте обсудим как хранить <code>mask[x]</code>. Во-первых, он занимает довольно много памяти. Сам массив имеет длину 2⋅10^4, и для каждого элемента нужно сохранить 10^5 бит. Суммарно это уже порядка 250мб, а memory limit в задаче 256 мегабайт. </p>
  <p id="Q7eI">Во-вторых, если мы хотим быстро пересчитывать массив <code>first</code>, то получение <code>i</code>-го бита <code>mask[x]</code> должно работать очень быстро (и использовать операции, которые можно ускорить с помощью simd). А в самом простом случае получение <code>i</code>-го бита из битсета требует каких-то битовых сдвигов, которые вряд ли соптимизируются. </p>
  <p id="WVTK">На самом деле <a href="https://www.felixcloutier.com/x86/vmaskmov" target="_blank">существуют</a> simd инструкции, которые могут проставить конкретные элементы в одном массиве из другого исходя из проставленных битов в маске. Но для этого компилятор должен догадаться, как трансформировать биты из битсета в формат, который нужен для этих инструкций. Если вы умеете помогать компилятору это делать без написания инструкций руками — расскажите мне :)</p>
  <p id="Bejs">Если же хранить <code>mask[x]</code> как просто массив <code>bool/u8</code>, то код, который обновляет <code>first</code>, будет хорошо векторизоваться. Но тогда он будет занимать в 8 раз больше памяти и не будет помещаться в 256 мегабайт.</p>
  <p id="2flR">Давайте не хранить все элементы <code>mask</code>, а только первые сколько-то элементов (например, 2400, чтобы все еще помещаться в ML). Если мы хотим узнать какой-нибудь <code>mask[a * b]</code>, то можно просто посчитать <code>mask[a] | mask[b]</code>. </p>
  <p id="7bcf">Как для конкретно <code>x</code> найти разложение <code>x = a * b</code> такое, чтобы <code>a</code> и <code>b</code> были не очень большими? Давайте посмотрим на наибольший простой делитель <code>x</code> (назовем его <code>p</code>). Если и для <code>p</code> и для <code>x/p</code> у нас есть подсчитанная маска, то все хорошо. Иначе <code>p</code> очень большое, а <code>mask[p]</code> состоит только из битов вида <code>p*i</code>, так что такую маску можно создать заново за время пропорциональное <code>N/p</code>.</p>
  <h2 id="rK9T">Реализация</h2>
  <p id="uVh3">Как и в <a href="https://teletype.in/@bminaiev/cp-simd-optimizations" target="_blank">прошлый раз</a> будем выделять горячие куски кода в отдельные функции и включать для них simd оптимизации. Например, функция обновление массива <code>first</code>  из какой-то маски будет выглядеть так:</p>
  <pre id="HCLJ" data-lang="rust">#[target_feature(enable = &quot;avx2&quot;)]
unsafe fn update_first(first: &amp;mut [u32], mask: &amp;[bool], new_value: u32) {
    for i in 0..first.len() {
        if mask[i] {
            first[i] = new_value;
        }
    }
}</pre>
  <p id="pjbQ">Подсчет ответа так:</p>
  <pre id="EtUO" data-lang="rust">#[target_feature(enable = &quot;avx2&quot;)]
unsafe fn calc_res(first: &amp;[u32], r: u32) -&gt; i32 {
    first.iter().map(|x| (*x &lt;= r) as i32).sum()
}</pre>
  <p id="qGjm">А главная часть решения так:</p>
  <pre id="Hfxx" data-lang="rust">for l in (0..n).rev() {
    let cur = a[l];

    unsafe {
        let l = l as u32;
        if cur &lt; mask.len() {
            update_first(&amp;mut first, &amp;mask[cur], l)
        } else {
            let p = largest_prime[cur];

            if p &lt; mask.len() {
                update_first(&amp;mut first, &amp;mask[cur / p], l);
                update_first(&amp;mut first, &amp;mask[p], l);
            } else {
                update_first(&amp;mut first, &amp;mask[cur / p], l);
                for i in (p..first.len()).step_by(p) {
                    first[i] = l;
                }
            }
        }
    }

    for query in queries[l].iter() {
        res[query.id] = max_v as i32 - unsafe { calc_res(&amp;first, query.r as u32) };
    }
}</pre>
  <p id="q3f6">Массив <code>first</code> имеет тип <code>[u32]</code>, а не <code>[usize]</code>, потому что <code>usize</code> 64 бита и работает медленнее. Поэтому приходится добавлять касты из одного типа в другой :(</p>
  <p id="joeX">К сожалению ровно в таком виде решение работает долго. На случайном тесте локально оно работает 1.7с, а на серверах CodeForces — 4.4c (TL в задаче 3с).</p>
  <h2 id="6Fnx">Профилируем</h2>
  <p id="6hD9">Почему такая большая разница между локальным временем работы и на КФ? </p>
  <p id="4fqu">Если бы у нас была возможность запускать любые программы на серверах КФ, можно было бы попробовать использовать <code>perf</code> или какую-нибудь другую утилиту, чтобы понять, что конкретно тормозит. Но ее нет.</p>
  <p id="3H9f">Вместо этого просто попробуем минимизировать пример, на котором видно проблему. Померяем отдельно сколько работают функции <code>update_first</code> и <code>calc_res</code>. Будем измерять скорость с помощью другой небольшой функции:</p>
  <pre id="LVgZ" data-lang="rust">#[inline(never)]
fn measure&lt;F&gt;(f: &amp;mut F)
where
    F: FnMut(),
{
    let start = Instant::now();
    const MAX_MILLIS: u128 = 1000;
    let mut iters = 0;
    while start.elapsed().as_millis() &lt; MAX_MILLIS {
        iters += 1;
        f();
    }
    println!(
        &quot;{} iters, av.time: {}mcs&quot;,
        iters,
        start.elapsed().as_secs_f64() / (iters as f64) * 1000.0 * 1000.0
    );
}</pre>
  <p id="VrVn">Сгенерируем случайные массивы и посмотрим, сколько будут работать функции:</p>
  <pre id="1W4w" data-lang="rust">pub fn main() {
    let mut rnd = Random::new(787788);
    const N: usize = 100_000;
    let mut first = vec![0; N];
    let mask = gen_vec(N, |_| rnd.gen_bool());
    measure(&amp;mut || unsafe { update_first(&amp;mut first, &amp;mask, 123) });
}</pre>
  <p id="GPp5">Результат:</p>
  <pre id="eMa7" data-lang="bash">70355 iters, av.time: 14.213780456257549mcs</pre>
  <p id="r0HC">Аналогично для calc_res:</p>
  <pre id="4C7F" data-lang="rust">pub fn main() {
    let mut rnd = Random::new(787788);
    const N: usize = 100_000;
    let first = gen_vec(N, |_| rnd.gen_u32(100));
    let mut hash = 0;
    measure(&amp;mut || unsafe {
        hash += calc_res(&amp;first, 50);
    });
}</pre>
  <p id="bBJv">В этом случае еще посчитаем какое-то число <code>hash</code>, чтобы компилятор не мог просто не выполнять код внутри <code>calc_res</code>.</p>
  <p id="eGER">Результат получается такой:</p>
  <pre id="lqXQ" data-lang="bash">115493 iters, av.time: 8.658555583455275mcs</pre>
  <p id="lyhu">В худшем случае наше решение 10^5 раз вызывает (2 раза <code>update_first</code> и 1 раз <code>calc_res</code>), что по идее должно работать 10^5 * (2 * 14.2 + 8.65) = 3.7c, а на случайных данных еще меньше. А решение в запуске на КФ почему-то работало 4.4с.</p>
  <p id="8Wzz">Секрет довольно прост — в случае с маленьким тестом мы запускаем <code>update_fist</code> на одном и том же массиве <code>mask</code>, а на реальных данных каждый раз берем случайный, который скорее всего не лежит в кеше.</p>
  <p id="vlRr">Чтобы проверить эту гипотезу можно немного переписать тест:</p>
  <pre id="SlPB" data-lang="rust">pub fn main() {
    let mut rnd = Random::new(787788);
    const N: usize = 100_000;
    let mut mask = vec![vec![false; N]; 2400];
    for it in 0..mask.len() {
        for i in 0..N {
            mask[it][i] = rnd.gen_bool();
        }
    }
    let mut first = vec![0; N];
    measure(&amp;mut || unsafe { update_first(&amp;mut first, &amp;mask[rnd.gen_usize(mask.len())], 123) });
}</pre>
  <p id="9qYB">Который работает в полтора раза дольше:</p>
  <pre id="EGAo" data-lang="bash">40216 iters, av.time: 24.866071638153972mcs</pre>
  <h2 id="Ar2m">Улучшаем</h2>
  <p id="Iopi">Теоретически, когда программа обращается к последовательным элементам массива, cpu hardware prefetcher должен заметить этот паттерн и подгружать следующие элементы массива в кеш до того как они потребуются. Но в данном случае почему-то этого не происходит.</p>
  <p id="GM3K">Но мы можем загрузить нужные элементы в кеш руками. Сделаем небольшую функцию, которая будет нам помогать:</p>
  <pre id="JgEe" data-lang="rust">fn prefetch(ptr: *const i8) {
    unsafe {
        core::arch::x86_64::_mm_prefetch::&lt;{ core::arch::x86_64::_MM_HINT_T0 }&gt;(ptr);
    }
}</pre>
  <p id="tr5A">Функция <code>_mm_prefetch</code> принимает не только адрес, но и уровень кеша, в который нужно загружать данные. Мы будем подгружать во все уровни сразу.</p>
  <p id="Y3TR">Перепишем функцию <code>update_first</code> с использованием <code>prefetch</code>. Самая простая версия может просто подгружать данные, которые находятся с каким-то сдвигом от текущих обрабатываемых элементов:</p>
  <pre id="pXhd" data-lang="rust">#[target_feature(enable = &quot;avx2&quot;)]
unsafe fn update_first(first: &amp;mut [u32], mask: &amp;[bool], new_value: u32) {
    const SHIFT: usize = 1024;
    for i in 0..first.len() {
        if mask[i] {
            first[i] = new_value;
        }
        prefetch(mask.as_ptr().add(i + SHIFT) as *const i8);
    }
}</pre>
  <p id="sMGI">Но такая версия работает гораздо хуже исходной. Как минимум потому, что обновление <code>first</code> хорошо векторизовалось, а <code>prefetch</code> делается для каждого отдельного байта. На самом деле этого делать не нужно, так как процессор подгружает сразу кеш линию в 64 байта. Так что можно вызывать его только каждую 64-ю итерацию. </p>
  <p id="sgun">Чтобы цикл все еще хорошо векторизовался можно разделить его на блоки. После обработки каждого блока, будем подгружать следующий блок в кеш.</p>
  <pre id="fEr9" data-lang="rust">#[target_feature(enable = &quot;avx2&quot;)]
unsafe fn update_first(first: &amp;mut [u32], mask: &amp;[bool], new_value: u32) {
    let n = first.len();
    let mask = &amp;mask[..n];
    const SHIFT: usize = 1024;
    for start in (0..n).step_by(SHIFT) {
        for i in start..min(start + SHIFT, n) {
            if mask[i] {
                first[i] = new_value;
            }
        }
        for it in 0..SHIFT / 64 {
            prefetch(mask.as_ptr().add(start + SHIFT + it * 64) as *const i8);
        }
    }
} </pre>
  <p id="AmIF">Такой код уже работает быстрее:</p>
  <pre id="x7Vd" data-lang="bash">51547 iters, av.time: 19.399798669175702mcs</pre>
  <p id="nIKI">Но почему-то он все еще работает дольше, чем когда используется один и тот же массив <code>mask</code>:(</p>
  <p id="JSIB">Но даже этого (плюс еще каких-то небольших оптимизаций) <a href="https://codeforces.com/contest/1718/submission/169446727" target="_blank">хватает</a>, чтобы получить АС на текущих тестах!</p>
  <tt-tags id="P6Hi">
    <tt-tag name="rust">#rust</tt-tag>
    <tt-tag name="simd">#simd</tt-tag>
    <tt-tag name="cp">#cp</tt-tag>
    <tt-tag name="prefetch">#prefetch</tt-tag>
  </tt-tags>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/google-ai4code</guid><link>https://teletype.in/@bminaiev/google-ai4code?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/google-ai4code?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>Google AI4Code или мой первый раз на Kaggle</title><pubDate>Thu, 11 Aug 2022 21:55:16 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a8/47/a847828c-3299-4856-a168-6f1ad3eca596.png"></media:content><tt:hashtag>kaggle</tt:hashtag><description><![CDATA[<img src="https://img2.teletype.in/files/51/4b/514b2ab7-4dce-4fa6-8499-03b01384ad9c.png"></img>Вчера наконец-то закончилось трехмесячное соревнование от гугла по машинному обучению, где нужно было написать программу, которая умеет понимать взаимосвязь между питоновским кодом и комментариями к нему. На вход вашей программе давался Python ноутбук, который состоит из клеток с кодом и клеток с текстом. Клетки с кодом даны в том же порядке, в котором они были в исходном ноутбуке. А клетки с текстом случайно перемешаны. Цель вашей программы — как можно точнее восстановить исходный порядок.]]></description><content:encoded><![CDATA[
  <p id="vctO">Вчера наконец-то закончилось трехмесячное <a href="https://www.kaggle.com/competitions/AI4Code/overview" target="_blank">соревнование</a> от гугла по машинному обучению, где нужно было написать программу, которая умеет понимать взаимосвязь между питоновским кодом и комментариями к нему. На вход вашей программе давался Python ноутбук, который состоит из клеток с кодом и клеток с текстом. Клетки с кодом даны в том же порядке, в котором они были в исходном ноутбуке. А клетки с текстом случайно перемешаны. Цель вашей программы — как можно точнее восстановить исходный порядок.</p>
  <p id="gMtt">До этого соревнования у меня не было никакого реального опыта машинного обучения, и я подумал, что это отличный шанс попробовать что-то новое. В итоге это оказалось достаточно прикольно и интересно, но очень выматывающе (хочется же получить результат получше, но ничего не работает, приходится пробовать много всего).</p>
  <p id="OzJV">Тестирование проходит в два этапа. Во время самого соревнования можно послать свой код и узнать, сколько он набирает баллов на закрытом датасете. Сам закрытый датасет набирался из публично доступный ноутбуков с Kaggle, так что теоретически можно было обучить свою модель на них и получить идеальный результат. Поэтому в следующие три месяца организаторы будут собирать новый датасет и перетестируют все решения на нем. </p>
  <p id="Un78">Так что официальных результатов ждать еще три месяца, но на предварительных у меня <strong>25е</strong> место из <strong>1000+</strong> команд. С одной стороны для первого раза довольно неплохо. С другой конечно же можно было гораздо лучше и судя по таблице результатов, решение явно можно сильно улучшить.</p>
  <h2 id="v8fd">О чем написать?</h2>
  <p id="5tuE">За прошедшие три месяца случилось довольно много интересных историй, и уместить их все в один пост явно не получится (ну либо его никто не дочитает до конца). Так что я постараюсь какую-то часть написать в этот пост, а потом, возможно, напишу еще отдельных историй (ставьте лайки и пишите о чем интересно почитать!). </p>
  <h2 id="clVz">Bad setup</h2>
  <p id="i9ls">Я потратил очень большое количество времени просто на то, чтобы сделать удобной разработку. Kaggle предоставляет возможность создавать jupyter ноутбуки и даже бесплатно запускать их. Они даже дают возможность пользоваться их gpu (30+ часов в неделю). Это прикольно, потому что можно сразу начать что-то делать, но на самом деле это ужасно не удобно. </p>
  <ul id="2JUh">
    <li id="CsYE">Во-первых, все тормозит. Запускаешь клетку ноутбука, ждешь секунду пока она исполнится. С одной стороны можно потерпеть, но с другой — это реально существенно замедляет процесс.</li>
    <li id="04NR">Во-вторых, когда ноутбук становится длиннее клеток 20, им становится невозможно пользоваться. Начинаешь запускать клетки в неправильном порядке, какие-то инварианты глобальных переменных ломаются, потом очень долго дебажишь.</li>
    <li id="qejb">В-третьих, нет нормальной возможности сохранить промежуточные данные куда-то на диск. Есть какое-то временное хранилище пока ноутбук работает, но если хочется переиспользовать данные, их нужно скачать/сохранить куда-то отдельно. По умолчанию, максимально ноутбук может работать 9 часов, а потом выключится. </li>
    <li id="JPXA">В-четвертых, нет нормальной возможности версионирования и хранения кода. Пусть у меня есть код, который генерирует какую-то модель, и код, который ее проверяет. На Kaggle это должно быть два отдельных ноутбука, чтобы второй мог использовать модель, который сгенерил первый. Но если я хочу переиспользовать часть кода (например, который считывает данные или считает статистику), его нужно будет скопировать.</li>
    <li id="R0jj">В-пятых, gpu, который Kaggle предоставляет бесплатно, <a href="https://www.techpowerup.com/gpu-specs/tesla-p100-pcie-16-gb.c2888" target="_blank">P100</a>, не то чтобы очень крутые.</li>
  </ul>
  <p id="nK3l">В общем, если бы я мог путешествовать во времени, и дать себе один совет, это явно был бы &quot;не использовать Kaggle для запуска кода&quot;. </p>
  <h2 id="IdiY">Better setup?</h2>
  <p id="NABq">Я вряд ли пришел к идеальному варианту, но в итоге получилось так:</p>
  <ul id="YHW0">
    <li id="rhII">Весь код лежит в <a href="https://github.com/BorysMinaiev/ai4-code" target="_blank">репозитории</a> на github. </li>
    <ul id="Fsit">
      <li id="khEE">Общий код лежит в .py файлах. </li>
      <li id="7dN3">Под каждый отдельный эксперимент есть свой .ipynb файл (в котором желательно не больше ~10 клеток). </li>
      <li id="LGIR">Чтобы подключать .py файлы из .ipynb, можно пользоваться этими магическими командами:</li>
    </ul>
  </ul>
  <pre id="26Ys" data-lang="python">%load_ext autoreload
%autoreload 2</pre>
  <ul id="YmEV">
    <li id="bIHi">Редактировал и писал новый код я в основном локально (latency гораздо лучше чем на Kaggle!).</li>
    <li id="snf3">Чтобы запускать код, я арендовал сервер на <a href="https://jarvislabs.ai/" target="_blank">https://jarvislabs.ai/</a></li>
    <ul id="myYS">
      <li id="JYy6">Сервер с gpu A6000 (у которого 48Gb памяти вместо 16Gb на Kaggle) стоит ~1$/час. В итоге я суммарно потратил где-то 200$, но я не особо пытался экономить.</li>
      <li id="wqmt">У них есть persistent storage, так что если сервер выключить, а потом включить — данные будут на месте. </li>
      <li id="MBfE">На сервер можно заходить по ssh. Оказывается у ssh есть замечательный ключ <code>-A</code>, который форвардит локальные ключи на сервер, к которому подключаешься. Например, если хочешь выкачать приватный github репозиторий на сервер, но не хочешь добавлять в github профиль ключ с этого сервера, то <code>-A</code> это то, что нужно.</li>
    </ul>
    <li id="WLrP">Когда все-таки хочется скачать с/на cервер что-то большое, можно пользоваться scp, но он тормозит (видимо потому что сервера стоят где-то в Индии), а хорошо работает <a href="https://github.com/prasmussen/gdrive" target="_blank">Google Drive Cli</a>. Правда безопасность этого решения несколько сомнительная. </li>
    <li id="oHty">Я не придумал как нормально сабмитить свое решение на Kaggle. Сабмитить нужно один ноутбук, а в репозитории код лежит в нескольких файлах. Если их в тупую собрать в один файл, то могут появиться функции с одинаковыми именами. Или будут какие-то лишние инклуды. В итоге я фиксил это все руками, но скорее всего можно как-то лучше.</li>
  </ul>
  <h2 id="h2zS">Графики!</h2>
  <p id="oieR">По жизни я очень люблю визуализировать данные и считаю, что обычно это путь к успеху. </p>
  <p id="SvDJ">В этот раз я пользовался <a href="https://wandb.ai/" target="_blank">https://wandb.ai/</a> и он очень клевый! В нем очень просто логировать данные во время экспериментов, а потом визуализировать и строить дашборды. </p>
  <p id="xgtB">Например как-то <a href="https://wandb.ai/bminaiev/ai4code/reports/scores-22-08-11-23-08-84---VmlldzoyNDYyNTQz?accessToken=082u8z4rl0m660ztnv1elyb5hc839k2qd3tv1m18t22i4c48ooco8xn31wff261d" target="_blank">так</a> выглядели графики с результатами разных моделей:</p>
  <figure id="Eb4v" class="m_original">
    <img src="https://img2.teletype.in/files/51/4b/514b2ab7-4dce-4fa6-8499-03b01384ad9c.png" width="3536" />
  </figure>
  <p id="AGfd">На самом деле он интерактивный и в нем можно что-то понять, честно-честно!</p>
  <p id="mE7q">Если зазумить, то будет так:</p>
  <figure id="DyNh" class="m_original">
    <img src="https://img3.teletype.in/files/e1/b8/e1b8e5da-43d7-407a-8f62-abd65ca88eab.png" width="3529" />
  </figure>
  <p id="G4f9">Тут на графике каждая линия — какая-то отдельная модель, которую тестируем. А значение — средний скор модели на Х ноутбуках. &quot;Настоящий&quot; скор модели мы бы получили, если бы посмотрели на значение при X=+infinity, но тестирование довольно долгое, поэтому хочется уметь быстрее понимать, получилась ли модель лучше чем другая. </p>
  <p id="oyGQ">Посмотрев на такой график можно понять, что тестирование, например, только на 100 ноутбуках, будет работать плохо и данные будут слишком шумные. А вот после 300 ноутбуков, если одна модель показывает результат лучше другой, то и настоящий результат скорее всего будет такой же. </p>
  <p id="xibE">Wandb позволяет строить графики интерактивно, т.е. прямо во время работы программы. Это спасает много времени, когда случайно запустил что-то неправильно, и сразу можешь заметить, что скор уж слишком маленький даже на первых ноутбуках.</p>
  <p id="xQF1">А еще иногда можно заметить, что график одной модели в точности совпадает с графиком другой, и понять, что в эксперименте явно что-то пошло не так (например, мы почему-то тестируем другую модель).</p>
  <h2 id="XlfH">В следующей серии?</h2>
  <ul id="Cmdt">
    <li id="BzlQ">Какое в итоге было решение.</li>
    <li id="ZR0I">Как я визуализировал данные и ничего полезного из этого не получил :(</li>
    <li id="Xu6q">Сколько раз я долго искал какие-то баги, потому что привык к нормальным языкам программирования, а не к питону.</li>
    <li id="VDUu">Как я пробовал сделать что-то более сложное, но ничего не работало :(</li>
  </ul>
  <tt-tags id="zkoN">
    <tt-tag name="kaggle">#kaggle</tt-tag>
  </tt-tags>
  <section style="background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="XKBy">Если вы дочитали до сюда, то подписывайтесь на мой канал <a href="https://t.me/bminaiev_blog" target="_blank">https://t.me/bminaiev_blog</a></p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/polyomino</guid><link>https://teletype.in/@bminaiev/polyomino?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/polyomino?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>Генерируем полимино</title><pubDate>Thu, 28 Jul 2022 17:31:08 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a6/5e/a65eb42e-c401-4796-8e7f-c5a5958d6a2d.png"></media:content><tt:hashtag>cp</tt:hashtag><tt:hashtag>rust</tt:hashtag><description><![CDATA[<img src="https://img2.teletype.in/files/94/b0/94b0cfab-8e93-4cc0-9ec4-9c02eb814d91.png"></img>В недавнем открытом кубке в задаче J в качестве подзадачи нужно было сгенерировать все возможные связные по стороне клеточные фигуры из не более чем четырех клеток. Во время контеста я так и не придумал, как написать код, который это делает, так, чтобы этот код вообще хотелось писать (и чтобы успеть это сделать минут за 5-10).]]></description><content:encoded><![CDATA[
  <p id="VJEG">В недавнем <a href="https://codeforces.com/blog/entry/105142" target="_blank">открытом кубке</a> в задаче J в качестве подзадачи нужно было сгенерировать все возможные связные по стороне клеточные фигуры из не более чем четырех клеток. Во время контеста я так и не придумал, как написать код, который это делает, так, чтобы этот код вообще хотелось писать (и чтобы успеть это сделать минут за 5-10).</p>
  <p id="GMIk">После конца контеста я вроде бы понял, что не все так страшно, так что давайте обсудим какие есть варианты.</p>
  <h2 id="Cqxq">Hardcode</h2>
  <p id="i2NQ">Еще во время контеста я подумал, что различных фигурок из 4 клеток не так много, и можно просто нарисовать их все на бумажке, а потом сделать константный массив в коде. Скорее всего такое решение будет даже самое короткое. Но у него есть очевидный недостаток — можно случайно забыть какую-то фигурку.</p>
  <p id="iTcu">В качестве упражнения я потом попытался это сделать. Если вам не лень, то можете проверить свою внимательность и понять, забыл ли я что-то :)</p>
  <figure id="cno9" class="m_column">
    <img src="https://img2.teletype.in/files/94/b0/94b0cfab-8e93-4cc0-9ec4-9c02eb814d91.png" width="1480" />
  </figure>
  <h2 id="S6Nv">Может лучше кодом?</h2>
  <p id="jWjB">Самое базовое решение, которое мне пришло в голову, выглядело так. Любая фигурка из 4 клеток должна помещаться в квадрат 4х4. Поэтому можно: </p>
  <ul id="1koj">
    <li id="cN22">перебрать все возможные закраски квадрата 4х4 (их всего 2^16)</li>
    <li id="HBRm">проверить, что фигура внутри состоит из не более чем 4 клеток</li>
    <li id="Yqur">проверить, что фигура связная</li>
  </ul>
  <p id="Sead">У этого метода есть проблема, что одну и ту же фигуру мы сгенерируем несколько раз. Например, прямоугольник 1х4 мы сгенерируем 4 раза (как каждую отдельную строку).</p>
  <p id="u2xA">Чтобы избавиться от этой проблемы, можно привести каждую фигурку к каноническому виду (например, сдвинув влево и вверх до упора) и сложить все фигуры в хештаблицу. </p>
  <p id="9Uqe">Другой способ — можно проверить, что у сгенерированной фигуры в первой строке и в первом столбце закрашено хотя бы по одной клетке. Тогда можно обойтись без хештаблицы. </p>
  <p id="l1cu">Немного деталей реализации. Будем хранить закрашенные клетки как один инт. Будем считать, что клетка <code>(row, col)</code> закрашена, если в этом инте проставлен <code>(row*4+col)</code>-й бит.</p>
  <p id="Hclz">Также будем считать, что у нас уже есть написанная структура данных <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure" target="_blank">Dsu</a>, которая умеет поддерживать множества. У <code>dsu</code> есть операция <code>unite</code>, которая принимает два элемента. Если они уже лежат в одном множестве, то возвращает <code>false</code>, а иначе объединяет множества и возвращает <code>true</code>.</p>
  <p id="9sUc">Код получается примерно такой:</p>
  <pre id="bzjk" data-lang="rust">fn gen_figures_mask(max_cnt: usize) {
    let id = |row: usize, col: usize| row * max_cnt + col;

    let first_row_mask = (1 &lt;&lt; max_cnt) - 1;
    let first_col_mask = (0..max_cnt).map(|row| 1 &lt;&lt; id(row, 0)).sum::&lt;i32&gt;();

    for mask in 1i32..(1 &lt;&lt; (max_cnt * max_cnt)) {
        if mask.count_ones() as usize &gt; max_cnt
            || (mask &amp; first_row_mask) == 0
            || (mask &amp; first_col_mask) == 0
        {
            continue;
        }
        let mut dsu = Dsu::new(max_cnt * max_cnt);
        let mut num_comps = mask.count_ones() as usize;
        for r in 0..max_cnt {
            for c in 0..max_cnt {
                let id1 = id(r, c);
                if (1 &lt;&lt; id1) &amp; mask != 0 {
                    if r + 1 &lt; max_cnt &amp;&amp; ((1 &lt;&lt; id(r + 1, c)) &amp; mask) != 0 {
                        if dsu.unite(id1, id(r + 1, c)) {
                            num_comps -= 1;
                        }
                    }
                    if c + 1 &lt; max_cnt &amp;&amp; ((1 &lt;&lt; id(r, c + 1)) &amp; mask) != 0 {
                        if dsu.unite(id1, id(r, c + 1)) {
                            num_comps -= 1;
                        }
                    }
                }
            }
        }
        if num_comps == 1 {
            // TODO: return figure
        }
    }
}</pre>
  <p id="J5f4">Итого код получился не очень длинный, но в нем довольно много каких-то битовых трюков и различных +- 1, в которых можно ошибиться. Плюс не очень понятно в каком именно формате возвращать ответ. </p>
  <h1 id="firstHeading">Breadth-first search</h1>
  <p id="VhsK">Другой возможный способ — написать bfs по фигурам. Начинаем с фигурки в одну клетку. А потом каждый раз добавляем новую клетку рядом с уже существующей. Такие фигуры будут автоматически связными. </p>
  <p id="6CY1">Но с таким подходом мы можем сгенерировать некоторые фигуры несколько раз. Чтобы этого не произошло, будем сохранять их в хештаблицу. При этом каждую фигуру будем хранить как вектор клеток, которые отсортированы лексикографически.</p>
  <p id="JCKt">Но проблема одинаковых фигур все еще может возникнуть. Например, мы начали с клетки <code>(0, 0)</code>, а потом добавили либо клетку <code>(1, 0)</code>, либо клетку <code>(-1, 0)</code>. В обоих случаях получилась одинаковая фигура. Чтобы такого не происходило можно считать, что клетка <code>(0, 0)</code> должна всегда присутствовать в фигуре, и что она должна быть лексикографически меньше всех остальных.</p>
  <p id="aA6Y">Предположим, что у нас уже есть структура <code>Point</code>, которая хранит пару <code>(x, y)</code>, а так же определена операция <code>+</code> на них.</p>
  <p id="ipP3">Тогда код будет примерно таким:</p>
  <pre id="HEnF" data-lang="rust">#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Figure(Vec&lt;Point&gt;);

impl Figure {
    pub fn new(mut pts: Vec&lt;Point&gt;) -&gt; Self {
        pts.sort();
        Self(pts)
    }
}

const SHIFTS_4: [Point; 4] = [
    Point { x: 0, y: 1 },
    Point { x: 0, y: -1 },
    Point { x: 1, y: 0 },
    Point { x: -1, y: 0 },
];

pub fn gen_figures(max_cnt: usize) -&gt; Vec&lt;Figure&gt; {
    let start = Figure::new(vec![Point::ZERO]);
    let mut seen = HashSet::new();
    let mut queue = VecDeque::new();
    seen.insert(start.clone());
    queue.push_back(start);
    while let Some(fig) = queue.pop_back() {
        if fig.0.len() == max_cnt {
            continue;
        }
        for p in fig.0.iter() {
            for shift in SHIFTS_4.iter() {
                let new_point = p + shift;
                if new_point &gt; Point::ZERO &amp;&amp; !fig.0.contains(&amp;new_point) {
                    let next_figure = Figure::new([fig.0.clone(), vec![new_point]].concat());
                    if !seen.contains(&amp;next_figure) {
                        seen.insert(next_figure.clone());
                        queue.push_back(next_figure);
                    }
                }
            }
        }
    }
    seen.into_iter().collect()
}</pre>
  <p id="k91X">В таком коде уже явно меньше какой-то битовой магии и сложнее ошибиться.</p>
  <h2 id="iuQp">Bonus</h2>
  <p id="rDZ0">Первый способ работал достаточно быстро, потому что 2^16 это не так много. Но если бы нужно было генерировать фигурки с большим количеством клеток, он бы работал значительно дольше. </p>
  <p id="R97r">Второй способ на каждую фигурку создает отдельный вектор и кладет его в хештаблицу, что тоже не самое лучшее решение с точки зрения скорости.</p>
  <p id="Rpv2">На самом деле существует способ генерации полимино, который работает за время пропорциональное их количеству и не требует хештаблицы. Оставим его придумывание в качестве упражнения.</p>
  <tt-tags id="OUtY">
    <tt-tag name="cp">#cp</tt-tag>
    <tt-tag name="rust">#rust</tt-tag>
  </tt-tags>
  <section style="background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="wKRJ">Если вы дочитали до сюда, то подписывайтесь на мой канал <a href="https://t.me/bminaiev_blog" target="_blank">https://t.me/bminaiev_blog</a></p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/local-optimizations</guid><link>https://teletype.in/@bminaiev/local-optimizations?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/local-optimizations?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>Local Optimizations Is All You Need</title><pubDate>Tue, 19 Jul 2022 16:11:21 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/f5/ca/f5ca0f10-75b1-410e-8880-feaa50d063b4.png"></media:content><tt:hashtag>cp</tt:hashtag><tt:hashtag>optimizations</tt:hashtag><description><![CDATA[<img src="https://img4.teletype.in/files/38/f9/38f92c41-363f-4ef3-a0b8-c4b422889af0.png"></img>Недавно maroonrk сказал, что задачу ARC142E не взяли в AtCoder Grand Contest (а взяли только в Regular), потому что боялись, что какие-то эвристические решения могут зайти. Мимо такого заявления нельзя было пройти спокойно, и надо было срочно загнать какую-то лажу в эту задачу.  ]]></description><content:encoded><![CDATA[
  <p id="ZtSf">Недавно <em>maroonrk</em> <a href="https://codeforces.com/blog/entry/104680?#comment-933742" target="_blank">сказал</a>, что задачу <a href="https://atcoder.jp/contests/arc142/tasks/arc142_e" target="_blank">ARC142E</a> не взяли в AtCoder Grand Contest (а взяли только в Regular), потому что боялись, что какие-то эвристические решения могут зайти. Мимо такого заявления нельзя было пройти спокойно, и надо было срочно загнать какую-то лажу в эту задачу.  </p>
  <p id="e1sT">Задача следующая. Есть два массива <code>a</code> и <code>b</code> (до 100 элементов в каждом, значения до 100), а также список пар <code>(i, j)</code>. Нужно поувеличивать элементы в <code>a</code> так, чтобы для каждой пары <code>(i, j)</code> из списка выполнялось хотя бы одно из двух условий:</p>
  <ul id="t6TW">
    <li id="jSqK"><code>a[i] &gt;= b[i] &amp;&amp; a[j] &gt;= b[j]</code>.</li>
    <li id="RgbE"><code>a[i] &gt;= b[j] &amp;&amp; a[j] &gt;= b[i]</code>.</li>
  </ul>
  <p id="UdCZ">При этом каждый раз когда увеличиваем <code>a[i]</code> на 1, платим одну монету. Нужно минимизировать количество потраченных монет.</p>
  <h2 id="YY8A">Общая идея</h2>
  <p id="Hcoz">Пусть мы откуда-то узнали, в каком порядке должны быть отсортированы <code>a[i]</code> в конце. Тогда восстановить сами <code>a[i]</code> оптимально довольно легко. Пусть информация про сортировку нам известна в формате массива <code>rank</code>, такого что: <code>rank[i] &lt; rank[j]</code> обозначает, что <code>a[i] &lt; a[j]</code>. Тогда если есть ограничение на пару <code>(i, j)</code>, то оптимально будет <code>a[i]</code> сделать хотя бы <code>min(b[i], b[j])</code>, а <code>a[j]</code> сделать хотя бы <code>max(b[i], b[j])</code>.</p>
  <p id="GQmL">Самая простая версия — будем генерировать случайную перестановку <code>rank</code>, строить ответ исходя из нее, считать стоимость. И повторять пока есть время. Это <a href="https://atcoder.jp/contests/arc142/submissions/33357585" target="_blank">решение</a> получает несколько <strong>АС</strong>, но большинство <strong>WA</strong>:</p>
  <figure id="bndF" class="m_original">
    <img src="https://img4.teletype.in/files/38/f9/38f92c41-363f-4ef3-a0b8-c4b422889af0.png" width="102" />
  </figure>
  <h2 id="VmLA">Локальные оптимизации</h2>
  <p id="wnz7">Локальные оптимизации это очень клевая техника, которая часто используется в marathon-style задачах, но иногда ее можно применить и в обычных контестах. Идея в том, чтобы немного изменять текущее решение, и применять только те изменения, которые улучшают итоговый результат.</p>
  <p id="6dpF">В случае с перестановками типичный пример локальной оптимизации — поменять местами два соседних элемента. Другая возможная оптимизация — взять случайный элемент, и переставить его в случайное место. Именно ее и будем использовать.</p>
  <p id="WEAt">Для простоты имплементации массив rank сделаем не перестановкой, а массивом даблов. Тогда основная часть решения будет выглядеть так:</p>
  <pre id="6H0K" data-lang="rust">let calc_score = |rank: &amp;[f64]| -&gt; usize {
    let mut cur_values = init_values.to_vec();
    let mut cur_res = 0;
    for (fr, to) in all_restrictions.iter() {
        let need = max(need[fr], need[to]);
        if rank[fr] &gt;= rank[to] &amp;&amp; cur_values[fr] &lt; need {
            cur_res += need - cur_values[fr];
            cur_values[fr] = need;
        }
    }
    cur_res
};

let mut rnd = Random::new(787788);
let mut rank = gen_vec(n, |_| rnd.gen_double());
let mut best_res = calc_score(&amp;rank);

let start_time = Instant::now();
while start_time.elapsed().as_millis() &lt; 1000 {
    let pos = rnd.gen(0..n);
    let new_val = rnd.gen_double();
    let old_val = rank[pos];
    rank[pos] = new_val;
    let new_res = calc_score(&amp;rank);
    if new_res &lt;= best_res {
        best_res = new_res;
    } else {
        rank[pos] = old_val;
    }
}</pre>
  <p id="f0wl">Такое решение уже работает гораздо лучше:</p>
  <figure id="VN9e" class="m_original">
    <img src="https://img1.teletype.in/files/07/9e/079e582e-f42a-43ca-bbea-ed8f3848ff37.png" width="102" />
  </figure>
  <p id="ltae">Во время дорешивания на AtCoder можно смотреть названия тестов, в данном случае они выглядят так (тут только часть тестов):</p>
  <figure id="YXnL" class="m_retina">
    <img src="https://img2.teletype.in/files/1f/06/1f066fdc-3413-419d-92af-559304a65893.png" width="299.5" />
  </figure>
  <p id="Fn8t">С одной стороны почти все случайные тесты это решение проходит, но с другой есть еще много <em>&quot;maximal&quot;</em> тестов, которые получают WA. Но есть один обнадеживающий момент — какой-то максимальный тест все-таки прошел:</p>
  <figure id="ABbj" class="m_original">
    <img src="https://img1.teletype.in/files/c3/d7/c3d798cf-df01-420a-acf6-7080cb98e97e.png" width="573" />
  </figure>
  <h2 id="jOj1">Оптимизации</h2>
  <p id="d0hy">Часто при написании локальных оптимизаций, нужно уметь быстро пересчитывать ответ при каждом изменении. Сейчас мы делаем это за <code>O(n^2)</code>, потому что должны посмотреть на каждое ребро. Но поскольку мы меняем <code>rank</code> только одного элемента, то можно пересчитать ответ за <code>O(n)</code>.</p>
  <p id="lrh3">Сам процесс оптимизации довольно скучный, так что не будем его тут описывать. </p>
  <p id="lX0k">Но важно понимать, что лучше всего начинать с простого решения, чтобы проверить, что локальные оптимизации вообще работают, и только потом начинать оптимизировать.</p>
  <p id="eeR4">После оптимизаций результат был такой:</p>
  <figure id="gfXc" class="m_original">
    <img src="https://img2.teletype.in/files/92/f5/92f519b8-6df0-47f2-b985-b6747e250a0a.png" width="106" />
  </figure>
  <p id="NvCV">После изменения rand seed, суммарный результат остался такой же, но разбиение по тестам было другое:</p>
  <p id="z4YQ"></p>
  <figure id="Zqac" class="m_column">
    <img src="https://img4.teletype.in/files/b9/14/b91457a0-ba4b-4d53-96bd-49aba67902bc.png" width="1174" />
  </figure>
  <p id="Y05h">Из этого можно было сделать вывод, что шанс есть :)</p>
  <p id="DTJd">Была идея, что можно сделать какие-то константные оптимизации, после которых решение успеет проверить больше перестановок, и в итоге получить <strong>АС</strong>.</p>
  <p id="ShR2">После довольно долгих оптимизаций результат был примерно такой:</p>
  <figure id="BsWP" class="m_original">
    <img src="https://img3.teletype.in/files/ab/61/ab6165bd-13be-409f-bff0-90241432add5.png" width="104" />
  </figure>
  <p id="arXk">Причем в зависимости от rand seed, не работает разный тест...</p>
  <h2 id="rEIN">Последняя оптимизация</h2>
  <p id="ofSK">Локальные оптимизации это хорошо, но еще лучше когда исходное решение уже сгенерировано хорошей эвристикой. Хотелось как-то учитывать, что если исходное значение элемента уже большое, то и <code>rank</code> должен быть скорее всего больше (но не всегда).</p>
  <p id="C06a">В итоге сработал какой-то такой код:</p>
  <pre id="10X6" data-lang="rust">fn gen_initial_ranks(rnd: &amp;mut Random, start: &amp;[usize], need: &amp;[usize]) -&gt; Vec&lt;usize&gt; {
    let n = start.len();

    let mx = rnd.gen(10..200i32);
    let a = rnd.gen(-mx..mx);
    let b = rnd.gen(-mx..mx);
    let mut scores = gen_vec(n, |pos| {
        (
            pos,
            start[pos] as i32 * a + need[pos] as i32 * b + rnd.gen(0..mx * mx),
        )
    });
    scores.sort_by_key(|(_, y)| *y);
    let mut ranks = vec![0; n];
    for i in 0..n {
        ranks[scores[i].0] = i;
    }
    ranks
}</pre>
  <p id="uO78">После этого решение уже заходит с довольно большим запасом по времени:</p>
  <figure id="kTwf" class="m_original">
    <img src="https://img4.teletype.in/files/be/67/be67dd29-3697-4368-893f-fb466c349700.png" width="333" />
  </figure>
  <h2 id="XPTk">Вывод</h2>
  <p id="A9FU">Лучше писать нормальные решения. Но когда правильное решение не придумывается, важно уметь запихивать и то, чего не хотели авторы задачи.</p>
  <tt-tags id="O0H0">
    <tt-tag name="cp">#cp</tt-tag>
    <tt-tag name="optimizations">#optimizations</tt-tag>
  </tt-tags>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/reproducible-benchmarks</guid><link>https://teletype.in/@bminaiev/reproducible-benchmarks?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/reproducible-benchmarks?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>Воспроизводимые бенчмарки</title><pubDate>Wed, 13 Jul 2022 17:22:35 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/2d/ae/2dae1cbd-ba4f-4c57-9db0-d88a82d6dbda.png"></media:content><tt:hashtag>rust</tt:hashtag><tt:hashtag>simd</tt:hashtag><description><![CDATA[<img src="https://img1.teletype.in/files/88/51/88512d36-bf7b-4331-8c6f-b7bb31fe59a9.png"></img>Недавно хотел понять, как лучше написать функцию, которая принимает два массива типа u8 и возвращает первую позицию, в которой они отличаются.]]></description><content:encoded><![CDATA[
  <p id="UNtb">Недавно хотел понять, как лучше написать функцию, которая принимает два массива типа <code>u8</code> и возвращает первую позицию, в которой они отличаются.</p>
  <p id="eY8F">Для простоты будем считать, что массивы одинакового размера. Самая простая версия на Rust выглядит так:</p>
  <pre id="KWSY" data-lang="rust">pub fn mismatch(s: &amp;[u8], t: &amp;[u8]) -&gt; Option&lt;usize&gt; {
    s.iter().zip(t.iter()).position(|(x, y)| x != y)
}</pre>
  <p id="TruW">Но, как можно увидеть <a href="https://rust.godbolt.org/z/PddPv7nWG" target="_blank">тут</a>, такой код не векторизуется, а честно проверяет символы один за одним. </p>
  <h2 id="wPsl">Как можно сделать лучше?</h2>
  <p id="7Ysm">С теоретической точки зрения все довольно просто. Зачем сравнивать по одному байту, если можно сравнивать несколько за раз? Например, в С++ мы могли бы скастить <code>char *</code> к <code>int *</code>, и сравнивать массивы уже как массивы интов. По сути так мы сравниваем за раз по 4 байта. Когда нашли первую отличающуюся четверку, можно отдельным проходом найти какой именно байт из 4 отличается.</p>
  <p id="0Y2i">И еще нужно отдельно обработать конец массива, если длина не делится на 4.</p>
  <p id="LDWn">На самом деле процессоры умеют обрабатывать больше 4 байт за раз. <code>ymm</code> регистры могут хранить сразу 256 бит (в 8 раз больше <code>i32</code>!). Так что можно разбить исходные массивы на блоки по 32 байта, сравнивать их с помощью <code>simd</code>, а когда нашли отличающийся блок, найти нужный байт в тупую.</p>
  <h2 id="Cgn3">chunks_exact</h2>
  <p id="5xoQ">В Rust есть специальный <a href="https://doc.rust-lang.org/core/arch/index.html" target="_blank">модуль</a> с интринскиками для использования <code>simd</code> напрямую, но код получается довольно сложно читаемый (да и писать его не очень приятно). </p>
  <p id="GCN0">Было бы гораздо лучше, если бы компилятор мог сам понять, что можно использовать <code>simd</code>, и сделал бы это явно лучше чем мы руками (учитывая какие инструкции знает текущий процессор и насколько они быстрые). К сожалению ни код выше, ни плюсовый аналог <a href="https://en.cppreference.com/w/cpp/algorithm/mismatch" target="_blank">std::mismatch</a>, компилятор не оптимизирует.</p>
  <p id="CI44">Но иногда можно написать код немного по-другому, чтобы компилятор понял, как именно нужно оптимизировать. В данном случае у <code>slice</code> есть функция <a href="https://doc.rust-lang.org/std/primitive.slice.html#method.chunks_exact" target="_blank">chunks_exact</a>, которая делит массив на куски одинакового размера (плюс какой-то хвост). После этого компилятор может понять, что у всех кусков одинаковый размер, и их можно сравнивать с помощью <code>simd</code>-инструкций. </p>
  <p id="k7jf">Итоговый <a href="https://rust.godbolt.org/z/aqaz9hc99" target="_blank">код</a> получается такой:</p>
  <pre id="KGor" data-lang="rust">pub fn mismatch_fast(s: &amp;[u8], t: &amp;[u8]) -&gt; Option&lt;usize&gt; {
    let len = s.len();

    const CHUNK_SIZE: usize = 32;
    let offset = s
        .chunks_exact(CHUNK_SIZE)
        .zip(t.chunks_exact(CHUNK_SIZE))
        .position(|(c1, c2)| c1 != c2)
        .unwrap_or(len / CHUNK_SIZE)
        * CHUNK_SIZE;

    s[offset..]
        .iter()
        .zip(t[offset..].iter())
        .position(|(c1, c2)| c1 != c2)
        .map(|x| x + offset)
}</pre>
  <p id="rSBm">Если посмотреть на генерируемый ассемблер, то вроде бы все ожидаемо, есть какой-то цикл, который загружает в <code>ymm0</code> регистр вначале данные из одного массива, потом <code>xor</code>-ит с данными из другого, проверяет получился ли 0, сдвигается на 32 байта в двух массивах и переходит к следующей итерации:</p>
  <figure id="K6ix" class="m_original" data-caption-align="center">
    <img src="https://img1.teletype.in/files/88/51/88512d36-bf7b-4331-8c6f-b7bb31fe59a9.png" width="549" />
    <figcaption>Цикл нормального человека</figcaption>
  </figure>
  <h2 id="U59p">Меряем скорость</h2>
  <p id="GKYm">Один из рекомендованных способов измерять производительность Rust кода это <a href="https://doc.rust-lang.org/cargo/commands/cargo-bench.html" target="_blank">cargo bench</a>.</p>
  <p id="I11A">Написал я тест:</p>
  <pre id="qCRu" data-lang="rust">#![feature(test)]
extern crate test;

#[inline(never)]
pub fn mismatch(s: &amp;[u8], t: &amp;[u8]) -&gt; Option&lt;usize&gt; {
    s.iter().zip(t.iter()).position(|(x, y)| x != y)
}

#[inline(never)]
pub fn mismatch_fast(s: &amp;[u8], t: &amp;[u8]) -&gt; Option&lt;usize&gt; {
    let len = s.len();

    const CHUNK_SIZE: usize = 32;
    let offset = s
        .chunks_exact(CHUNK_SIZE)
        .zip(t.chunks_exact(CHUNK_SIZE))
        .position(|(c1, c2)| c1 != c2)
        .unwrap_or(len / CHUNK_SIZE)
        * CHUNK_SIZE;

    s[offset..]
        .iter()
        .zip(t[offset..].iter())
        .position(|(c1, c2)| c1 != c2)
        .map(|x| x + offset)
}

#[cfg(test)]
mod tests {
    use super::*;
    use test::Bencher;

    fn gen_inputs() -&gt; (Vec&lt;u8&gt;, Vec&lt;u8&gt;) {
        const LEN: usize = 200_000;
        const CHANGED_POSITION: usize = 100_500;

        let s: Vec&lt;u8&gt; = (0..LEN).map(|x| (x % 10) as u8).collect();
        let mut t = s.clone();
        t[CHANGED_POSITION] = 1;
        assert_ne!(s, t);
        (s, t)
    }

    #[bench]
    fn simple(b: &amp;mut Bencher) {
        let (s, t) = gen_inputs();
        b.iter(|| {
            mismatch(&amp;s, &amp;t).unwrap();
        });
    }

    #[bench]
    fn fast(b: &amp;mut Bencher) {
        let (s, t) = gen_inputs();
        b.iter(|| {
            mismatch_fast(&amp;s, &amp;t).unwrap();
        });
    }
}</pre>
  <p id="BRcQ">Запускаю:</p>
  <pre id="Zc21" data-lang="bash">$ cargo bench --quiet

running 2 tests
test tests::fast   ... bench:      48,570 ns/iter (+/- 423)
test tests::simple ... bench:      52,240 ns/iter (+/- 3,557)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 1.05s</pre>
  <p id="QTqJ">Хмм, оба способа работают примерно 50мкс, но должно же было быть в 32 раза быстрее? Ну ладно, не в 32, все-таки simd инструкции, наверное, работают чуть дольше чем обычные сравнения, но не настолько же!</p>
  <p id="S5Nk">Так, ну ладно, давайте запустим <code>perf</code> на коде, который должен был работать быстро:</p>
  <pre id="fwt9" data-lang="bash">$ perf record cargo bench --quiet fast
...
$ perf report</pre>
  <p id="DUY2">Вижу я там примерно такое:</p>
  <figure id="jGR4" class="m_retina">
    <img src="https://img4.teletype.in/files/30/9a/309af0d0-7ed7-4456-bb43-a0a16c5e5904.png" width="276.5" />
  </figure>
  <p id="9geS">Но куда делся тот прекрасный простой цикл, который мы видели в Compiler Explorer?</p>
  <h2 id="4NxK">Учимся смотреть ассемблер правильно</h2>
  <p id="Zfac">Смотреть на ассемблер в выводе <code>perf</code> конечно прикольно, но должен же быть какой-то более простой способ?</p>
  <p id="9q3w">Раньше для этого был <a href="https://github.com/gnzlbg/cargo-asm" target="_blank">cargo asm</a>, который вроде бы больше не поддерживается и плохо работает с <code>workspaces</code>. Зато появился какой-то <a href="https://www.reddit.com/r/rust/comments/u3g0ih/new_crate_announcement_cargoshowasm/" target="_blank">cargo-show-asm</a>. Им и будем пользоваться.</p>
  <pre id="kqbz" data-lang="bash">$ cargo asm test_diff::mismatch_fast</pre>
  <figure id="Txu2" class="m_original">
    <img src="https://img3.teletype.in/files/a0/2e/a02ec8a0-1412-4675-8af0-036c13f99aa5.png" width="626" />
  </figure>
  <p id="5SY9">В принципе это очень похоже на тот код, который был в Compiler explorer. Но почему в <code>perf</code> мы видели какой-то другой? </p>
  <p id="5yqC">На самом деле <code>cargo bench</code> и <code>cargo asm</code> используют разные опции для компиляции (<code>RUSTFLAGS</code>), что может приводить к подобным эффектам. Давайте уберем <code>RUSTFLAGS</code> вообще:</p>
  <pre id="y6YA" data-lang="bash">$ RUSTFLAGS=&quot;&quot; cargo bench --quiet 

running 2 tests
test tests::fast   ... bench:       2,989 ns/iter (+/- 77)
test tests::simple ... bench:      52,195 ns/iter (+/- 579)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 1.72s</pre>
  <p id="6iJF">Хмм, теперь <code>fast</code> версия действительно быстрая, но что же было такое плохое записано в <code>RUSTFLAGS</code>?</p>
  <p id="7zPn">На самом деле в <code>~/.cargo/config</code> у меня прописано, что нужно оптимизировать под локальный процессор. Поэтому воспроизвести это поведение можно так:</p>
  <pre id="uCIK" data-lang="bash">$ RUSTFLAGS=&quot;-C target-cpu=native -O&quot; cargo bench --quiet 

running 2 tests
test tests::fast   ... bench:      48,568 ns/iter (+/- 3,422)
test tests::simple ... bench:      52,410 ns/iter (+/- 1,354)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 1.10s</pre>
  <p id="p5RH">Очень подозрительно, что когда мы даем больше свободы компилятору, он начинает генерировать более медленный код. А еще в Compiler Explorer мы тоже передавали <code>target-cpu=native</code>, и он генерировал нормальный код.</p>
  <p id="NJQW">Но давайте все-таки посмотрим на код, который генерируется во время <code>cargo bench</code>. <a href="https://stackoverflow.com/a/41332159" target="_blank">Говорят</a> можно просто передать <code>--emit=asm</code> в <code>RUSTFLAGS</code>, так и сделаем:</p>
  <pre id="2Jzr" data-lang="bash">$ RUSTFLAGS=&quot;-C target-cpu=native -O --emit=asm&quot; cargo bench --quiet 

running 2 tests
test tests::fast   ... bench:       2,577 ns/iter (+/- 42)
test tests::simple ... bench:      53,892 ns/iter (+/- 2,685)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 2.76s</pre>
  <p id="Ln8J">и потом в <code>target/release/deps/test_diff-af77c0bfbd72c9e5.s</code> можно найти уже знакомый цикл:</p>
  <figure id="es90" class="m_original">
    <img src="https://img1.teletype.in/files/88/62/8862d0c4-cb90-4f80-80c1-1d31abbc5432.png" width="336" />
  </figure>
  <p id="PVPE">Так, стоп, но это же быстрая версия? Мы же ожидали увидеть простыню, которую видели в выводе <code>perf</code>. Хмм, и судя по результату теста, она реально работает быстро (причем даже быстрее чем когда передавали пустой <code>RUSTFLAGS</code>). Т.е. получается от того, что мы захотели посмотреть на <code>asm</code> и передали <code>--emit=asm</code>, все стало работать быстрее?</p>
  <p id="T1X0">На самом деле <code>--emit=asm</code> неявно форсит флаг <code>codegen-units=1</code>, поэтому можно передать его сразу и получить такой же эффект:</p>
  <pre id="vA1M" data-lang="bash">$ RUSTFLAGS=&quot;-C target-cpu=native -O -C codegen-units=1&quot; cargo bench --quiet 

running 2 tests
test tests::fast   ... bench:       2,501 ns/iter (+/- 164)
test tests::simple ... bench:      53,446 ns/iter (+/- 592)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 1.12s</pre>
  <p id="69Df">Скорее всего он же передается в Compiler explorer, поэтому мы видели там быструю версию кода.</p>
  <h2 id="AX7Q">Больше оптимизаций</h2>
  <p id="bTXN">Говорят, чем с большем уровнем оптимизаций компилировать код, тем быстрее он будет работать. Но на самом деле если передать <code>opt-level=3</code>:</p>
  <pre id="0mSF" data-lang="bash">$ RUSTFLAGS=&quot;-C target-cpu=native -O -C codegen-units=1 -C opt-level=3&quot; cargo bench --quiet 

running 2 tests
test tests::fast   ... bench:      49,108 ns/iter (+/- 970)
test tests::simple ... bench:      53,382 ns/iter (+/- 693)

test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured; 0 filtered out; finished in 0.55s</pre>
  <p id="10bP">он опять станет медленным.</p>
  <p id="YL61">Кстати, его же можно передать в Compiler Explorer и <a href="https://rust.godbolt.org/z/vdhj9s4do" target="_blank">увидеть</a> к чему приводят оптимизации.</p>
  <h2 id="Uy7H">Вывод</h2>
  <figure id="PCAU" class="m_original">
    <img src="https://img3.teletype.in/files/2a/16/2a160b8d-a8f2-494f-96db-55fea660cf6e.png" width="380" />
  </figure>
  <tt-tags id="crSU">
    <tt-tag name="rust">#rust</tt-tag>
    <tt-tag name="simd">#simd</tt-tag>
  </tt-tags>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@bminaiev/cp-simd-optimizations</guid><link>https://teletype.in/@bminaiev/cp-simd-optimizations?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev</link><comments>https://teletype.in/@bminaiev/cp-simd-optimizations?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=bminaiev#comments</comments><dc:creator>bminaiev</dc:creator><title>O(N^2)</title><pubDate>Fri, 08 Jul 2022 19:47:18 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a9/a1/a9a121de-a704-479c-8cc8-546adbb3c2a2.png"></media:content><tt:hashtag>rust</tt:hashtag><tt:hashtag>cp</tt:hashtag><tt:hashtag>simd</tt:hashtag><description><![CDATA[<img src="https://img1.teletype.in/files/c8/d3/c8d3832a-bf1b-4bf4-8470-329019250840.png"></img>Каждый раз, когда вижу в олимпиадных задачах ограничение N≤10^5, хочется проверить, стали ли компьютеры достаточно быстрыми, чтобы O(N^2) успел по времени. ]]></description><content:encoded><![CDATA[
  <p id="VyaT">Каждый раз, когда вижу в олимпиадных задачах ограничение N≤10^5, хочется проверить, стали ли компьютеры достаточно быстрыми, чтобы O(N^2) успел по времени. </p>
  <p id="ST3N">Я не видел особо туториалов по тому, как использовать <a href="https://ru.wikipedia.org/wiki/SIMD" target="_blank">SIMD</a> в олимпиадах, так что решил куда-то записать свой опыт.</p>
  <p id="fyu5">Когда я прочитал задачу <a href="https://codeforces.com/contest/1701/problem/F" target="_blank">F с Edu131</a>, Time Limit = 6.5с показался слишком привлекательными...</p>
  <h2 id="Beub">Задача</h2>
  <p id="gyTC">Если отбросить детали, то её можно переформулировать следующим образом. Есть два массива <code>alive</code> (добавлена ли сейчас точка в множество) и <code>cnt</code> (количество живых точек правее на расстоянии не более <code>d</code>), каждый размером 2·10^5. Нужно уметь добавлять/удалять точки из множества. Т.е:</p>
  <ul id="iEzY">
    <li id="nXDs">Обновить <code>alive</code></li>
    <li id="EKIE">На некотором отрезке слева от текущей точки добавить или отнять 1 от <code>cnt</code>.</li>
  </ul>
  <p id="3Fv7">После каждой операции нужно считать сумму по всем живым точкам <code>cnt[i] * (cnt[i] - 1) / 2</code>.</p>
  <h2 id="lwin">Baseline</h2>
  <p id="svn6">Сразу скажу, что делать нормальные воспроизводимые бенчмарки мне лень, так что погрешность измерений может быть довольно большой. Иногда измерения будут с ноута, а иногда с запуска на CodeForces (где все работает в ~2 раза медленнее и другое окружение). А еще все примеры кода будут на Rust, но должно быть все понятно.</p>
  <p id="NhHj">Тестировать будем на тесте, когда для всех <code>i</code> от <code>1</code> до <code>N</code>, точку <code>i</code> добавляют в множество, и нужно обновить все значения <code>cnt</code> левее <code>i</code>. Вроде бы это максимальный тест, который может быть в исходной задаче.</p>
  <p id="qbsO">Самый простой вариант обработки одного запроса <code>query</code> выглядит примерно так:</p>
  <pre id="Jir9" data-lang="rust">alive[query] = !alive[query];
let delta = if alive[query] { 1 } else { -1 };
for c in cnt[seg_start[query]..query].iter_mut() {
    *c += delta;
}
let res = cnt
    .iter()
    .zip(alive.iter())
    .map(|(&amp;cnt, &amp;alive)| if alive { cnt * (cnt - 1) / 2 } else { 0 })
    .sum();</pre>
  <p id="370J">Локально он работает <strong>~11.1с</strong>, на CodeForces это будет секунд 20, так что точно не вариант.</p>
  <h2 id="ZdHj">Простые оптимизации</h2>
  <p id="nbjO">Некоторые считают, что <code>if</code> в самом горячем месте кода это всегда плохо, и если его заменить, например, на умножение, все станет гораздо быстрее. На самом деле это не так, и в нашем случае все будет работать только медленнее. По крайней мере на этом тесте <code>alive</code> у нас всегда <code>true</code>, так что бранч предиктор будет очень хорошо предсказывать этот переход, и он почти не будет влиять на скорость.</p>
  <p id="VTuB">А вот деление это очень плохо, так что если не делить на два внутри <code>map</code>, а сделать это только один раз в конце, то тест отработает за <strong>~8.5с</strong>.</p>
  <h2 id="eyt3">Perf</h2>
  <p id="UVwj">Умение пользоваться <code>perf</code>-ом может сильно помочь в поиске проблемных кусков кода. Но довольно часто <code>perf</code> выдает какую-то чушь, и нужно уметь это чинить. Базовые правила использования <code>perf</code>-а для <code>Rust</code>:</p>
  <ul id="pQ2e">
    <li id="Gegw">В <code>Cargo.toml</code> у workspace-а или проекта нужно обязательно добавить:</li>
  </ul>
  <pre id="WTaz" data-lang="toml">[profile.release]
debug = 1</pre>
  <p id="5Deq">Иначе он не будет показывать строки исходного кода, которые соответствуют asm-у.</p>
  <ul id="6BSk">
    <li id="t7vL">В <code>~/.cargo/config</code> дописать:</li>
  </ul>
  <pre id="5JUl" data-lang="toml">[build]
rustflags = &quot;-C force-frame-pointers=yes&quot;</pre>
  <p id="yIQB">Возможно так perf будет лучше понимать откуда какая функция вызвалась и лучше строить стектрейсы. На скорость вроде бы влиять особо не должно.</p>
  <ul id="WY05">
    <li id="01GL"><code>perf record</code> нужно запускать с флагом <code>-g</code>, чтобы записывались стектрейсы.</li>
    <li id="6C30">Еще в <code>perf record</code> можно добавлять <code>--call-graph dwarf</code>, но лично у меня почему-то после этого <code>perf report</code> долго запускается.</li>
  </ul>
  <p id="dsM7">Итак, в perf-е текущей версии есть два горячих места:</p>
  <figure id="FRYK" class="m_column" data-caption-align="center">
    <img src="https://img1.teletype.in/files/c8/d3/c8d3832a-bf1b-4bf4-8470-329019250840.png" width="528" />
    <figcaption>Обновление cnt</figcaption>
  </figure>
  <figure id="tTMx" class="m_column" data-caption-align="center">
    <img src="https://img3.teletype.in/files/28/19/28198ddf-44d3-400f-8a8c-d8cad01a7eec.png" width="901" />
    <figcaption>Подсчет текущего результата</figcaption>
  </figure>
  <p id="g9py">Какие можно сразу сделать выводы?</p>
  <ul id="ULvB">
    <li id="3Czy">Судя по использованию ymm регистров и буквe p (packed) в названии некоторых инструкций — тут уже есть SIMD! Компилятор умный (?) </li>
    <li id="wJVn">Пересчет результата занимает явно больше времени чем обновление <code>cnt</code> (в столбце слева процент времени, который программа провела в этой строке).</li>
  </ul>
  <h2 id="u83r">Упрощаем (жизнь процессору, не код)</h2>
  <p id="cfTK">Как улучшить подсчет результата? </p>
  <p id="t5Oo">Во-первых, можно пересчитывать его только для части массива, на котором он поменялся (оптимизация в два раза на нашем тесте!).</p>
  <p id="XAvy">Во-вторых, можно только считать разницу между старым результатом и новым, тогда не нужно будет делать умножения в самом вложенном месте.</p>
  <p id="Oull">Получился явно не самый простой для понимания код:</p>
  <pre id="PuLA" data-lang="rust">alive[query] = !alive[query];

let delta0 = if alive[query] { 0 } else { -1 };
let delta = if alive[query] { 1 } else { -1 };

res += cnt[seg_start[query]..query]
    .iter()
    .zip(alive[seg_start[query]..query].iter())
    .map(|(&amp;cnt, &amp;alive)| if alive { cnt + delta0 } else { 0 })
    .sum::&lt;i64&gt;()
    * 2
    * delta;
    
res += delta * cnt[query] * (cnt[query] - 1);
    
for c in cnt[seg_start[query]..query].iter_mut() {
    *c += delta;
}</pre>
  <p id="tdyR">Но основные идеи довольно просты:</p>
  <ul id="t1de">
    <li id="nYm5">Заводим глобальную переменную <code>res</code> на все запросы.</li>
    <li id="KQIN">Отдельно обрабатываем вклад текущей точки.</li>
    <li id="n2Vx">(с точностью до +-1) добавляем к ответу сумму <code>cnt</code> у живых точек слева, потому что это то, на сколько поменялись <code>cnt[i] * (cnt[i] - 1)</code> при изменении <code>cnt[i]</code> на 1.</li>
  </ul>
  <p id="22CD">Такое решение работает локально <strong>~4.7c</strong>, что очень обнадеживает. Но на CodeForces не укладывается даже в 15с, очень жаль.</p>
  <h2 id="XzZo">Что не так с CodeForces?</h2>
  <p id="Wtr0">Когда пытаешься что-то оптимизировать, полезно выделить важный кусок кода в отдельную функцию, которая ни от чего не зависит и у который понятный интерфейс. В нашем случае выделим две функции:</p>
  <pre id="UQmc" data-lang="rust">pub fn add_const(arr: &amp;mut [i64], delta: i64) {
    for val in arr.iter_mut() {
        *val += delta;
    }
}

pub fn calc_res(alive: &amp;[bool], cnt: &amp;[i64], delta0: i64) -&gt; i64 {
    cnt.iter()
        .zip(alive.iter())
        .map(|(&amp;cnt, &amp;alive)| if alive { cnt + delta0 } else { 0 })
        .sum()
}</pre>
  <p id="cAvi">После этого на них можно смотреть в <a href="https://rust.godbolt.org/z/v9cKMTMxs" target="_blank">Compiler Explorer</a>. Например, можно заметить, что функция <code>add_const</code> использует <code>xmm</code> регистры (обычно это хороший знак), а <code>calc_res</code> — нет. Т.е. <code>calc_res</code> совсем не использует никакой SIMD магии. Но почему?</p>
  <p id="kvSJ">По умолчанию компилятор раста очень консервативен относительно того, какие инструкции он использует. Это нужно, чтобы программа, которую скомпилировали на одном компьютере, могла запускаться на другом. Даже если ваш процессор супер-пупер новый и поддерживает кучу клевых быстрых инструкций, по умолчанию раст вместо них будет использовать старые и проверенные.</p>
  <p id="UuhO">Если вы уверены, что программа будет запускаться на том же железе, на котором компилируется, то можно передать <code>-C target-cpu=native</code> в строку компиляции. Это можно сделать и в <a href="https://rust.godbolt.org/z/dxhPcW4d4" target="_blank">compiler explorer</a> и увидеть, что теперь <code>calc_res</code> использует <code>xmm/ymm</code> регистры.</p>
  <p id="TEBe">Но есть проблема, что мы не можем поменять флаги, с которыми компилируется наша программа на CodeForces. Зато в Rust есть возможность внутри кода сказать компилятору, чтобы он использовал модные инструкции. Но если во время исполнения окажется, что их нет, программа как-то упадет, так что такой код автоматически становится <code>unsafe</code>. Примерно так:</p>
  <pre id="Ny7l" data-lang="rust">#[target_feature(enable = &quot;avx2&quot;)]
pub unsafe fn add_const(arr: &amp;mut [i64], delta: i64) {
    for val in arr.iter_mut() {
        *val += delta;
    }
}

#[target_feature(enable = &quot;avx2&quot;)]
pub unsafe fn calc_res(alive: &amp;[bool], cnt: &amp;[i64], delta0: i64) -&gt; i64 {
    cnt.iter()
        .zip(alive.iter())
        .map(|(&amp;cnt, &amp;alive)| if alive { cnt + delta0 } else { 0 })
        .sum()
}</pre>
  <p id="3LiF">Аналогичная проблема есть и на других тестирующих системах, но нужно быть осторожным и использовать только те расширения, которые там действительно есть. Например, на Yandex Contest прогресс остановился на </p>
  <pre id="8oJe" data-lang="rust">#[target_feature(enable = &quot;sse2&quot;)]</pre>
  <p id="xw2t">Версия кода, которая использует <code>avx2</code>, в запуске на CF работает уже <strong>7.7с</strong>, а не больше 15 как раньше! Напомню, что TL в задаче 6.5c, так что осталось совсем чуть-чуть.</p>
  <h2 id="DAMC">64/32</h2>
  <p id="BXJR">Можно заметить, что <code>cnt</code> всегда помещается в 32 бита, так что можно использовать <code>[i32]</code>. К сожалению, сумма элементов уже не влазит в <code>i32</code>, так что нужно не забыть добавить много кастов по всему коду к <code>i64</code>.</p>
  <p id="Qupq">Такой код работает уже порядка <strong>6.5с</strong> в запуске на CF. Возможно идея в том, что больше 32-битных чисел помещается в один <code>xmm/ymm</code> регистр.  А возможно на CF все еще используют 32-битные что-то и это как-то влияет? Но факт остается фактом, оптимайз действительно помогает.</p>
  <p id="WCKA">К сожалению эти 6.5с из запуска не учитывают считывание, вывод, и случайные изменения времени работы от теста к тесту, так что нужно еще немного соптимизировать.</p>
  <h2 id="qIQU">Последняя оптимизация</h2>
  <p id="W3Qm">Мне все еще хотелось сделать <code>alive</code> типом <code>[i32]</code> и заменить <code>if</code> на умножение, но это по прежнему только замедляло программу.</p>
  <p id="JCLv">Но на самом деле вместо умножения можно использовать битовые операции. Например, сказать, что в <code>alive</code> мы храним либо <code>0</code>, либо <code>-1</code>, а при подсчете делаем <code>&amp;</code> с ним. Финальная версия выглядит как-то так:</p>
  <pre id="ycHt" data-lang="rust">#[target_feature(enable = &quot;avx2&quot;)]
pub unsafe fn calc_res(alive: &amp;[i32], cnt: &amp;[i32], delta0: i32) -&gt; i64 {
    cnt.iter()
        .zip(alive.iter())
        .map(|(&amp;cnt, &amp;alive)| (alive &amp; (cnt + delta0)) as i64)
        .sum()
}</pre>
  <p id="0OA0">Она работает <strong>~6.2c</strong> и должна стабильно <a href="https://codeforces.com/contest/1701/submission/163308807" target="_blank">получать AC</a>.</p>
  <h2 id="yx6Y">Вывод</h2>
  <p id="qY7t">Скорее всего у этого текста целевая аудитория 1 человек и это я сам, но хорошо хоть записал :)</p>
  <p id="O0ke"></p>
  <p id="8VDO">Обсуждать можно <a href="https://t.me/bminaiev_blog/5" target="_blank">тут</a> или на <a href="https://codeforces.com/blog/entry/104735" target="_blank">CF</a>.</p>
  <tt-tags id="ZsX0">
    <tt-tag name="rust">#rust</tt-tag>
    <tt-tag name="cp">#cp</tt-tag>
    <tt-tag name="simd">#simd</tt-tag>
  </tt-tags>

]]></content:encoded></item></channel></rss>