DC Randonneurs Frederick 300 km Brevet Ride Report

Here's the route map.

Three weeks after a very slow and very cold Warrenton 300 pre-ride, I decided to ride the Frederick 300. I wasn't sure if this was the best choice — with only two rides longer than my 11-mile commute leg all year, a 200 km permanent might make more sense. But I finished Warrenton, and the numbness in my left index finger had mostly gone away, so why not? I figured the worst thing that was likely to happen was being very slow over the early climbs and riding alone in the back all day.

It was about 45F at the start with a forecast high around 80, and then a forecast low Saturday night around 44. If I weren't so slow this year I would have just dressed for 45 and not brought anything else, but after almost freezing my hands off at the end of the Warrenton pre-ride when it got down into the low 20s, I wasn't taking any chances. I brought several items of just-in-case winter clothing: lobster claws, balaclava, rain jacket. I considered bringing a Camelbak, because of the forecast high temperature and gaps of up to 41 miles between stops, but decided that two Zefal Magnum bottles (advertised at 1L of capacity, but actually only holding 28 oz.; caveat emptor) were enough.

We got a good-sized group: about 40 riders. Impressive considering we have 3 300s this spring. With most of the climbing in the first third of the ride, my strategy was to avoid burning any extra energy at the start so I would have something left for the last 100 miles. With that in mind, I started near the back of the pack and did my best to stay there. The ride started at 5 a.m. and went through empty downtown Warrenton, with the big group running every red light after verifying the lack of cross traffic. I slipped off the back early on some small hills, and by mile 3 got permanently separated from the big group when they crossed US 15 a few seconds before me, then I had to wait for cross traffic to clear. Riding alone, with my poorly aimed helmet light making reading the cue sheet a challenge, I then made a navigational error and did 7/10 of a bonus mile. (I need to mount a small reading light on my handlebars before the next night ride.)

I was caught by another small group of riders I didn't know, and rode with them until one of them unexpectedly slammed on his brakes right after the metal plate bridge at mile 14. (Not sure why.) This nearly caused a crash, and I decided to drop off that group for safety. I got caught by Nick and about half a dozen others in Thurmont, and rode with them up the start of the Catoctin Mountain climb. I needed to pee, though, so the first time I saw a nice sheltered spot I let them go, and wouldn't see them again for the rest of the ride.

I rode up Catoctin Mountain a bit more, and saw one rider ahead climbing as slowly as me, but then I noticed my speedometer reading zero, and had to pull over to adjust the sensor. (The speedometer isn't critical, but the odometer helps a lot with navigation.) That meant I no longer had any riders to chase, which was probably good since I was trying to save energy. I went over that climb very slowly but with no drama, and plowed along slowly. On a small roller a bit later, with the sun in my eyes, I stopped to put on my sunglasses, and Bill S. passed me. Didn't know there was anyone behind me. He was going faster than I wanted to so I stuck to my plan and let him go.

At the mile 31 control I had a Vanilla Coke. It was still cold and I wasn't very hungry or thirsty, but I figured a bit of sugar and salt and water wouldn't hurt. Bill was there when I arrived, and Ray arrived while I was still there, but we all left separately. (Ray lost some time looking for a bathroom that was allegedly at a local community center, but which was locked up. Controls without public bathrooms suck. I'm going to change my policy of always buying something at controls to only always buying something at controls with bathrooms.)

I got a bit confused at the turn onto Jacks Mountain Road, but eventually ended up in the right place (which unfortunately was a few hundred feet up). Warmed up by the climb, I stopped at the Sunoco at mile 41 to remove a layer of clothing and turn off my lights. Ray caught me again there, but I left before him again. I had another minor navigational mishap at the mile 47 turn onto Railroad Lane; I saw the road but not a street sign for it, and the cue didn't say "unmarked," so I continued up Orrtanna Rd. for a couple more tenths looking for another right turn, didn't see one, then doubled back. No big deal, but another half-mile wasted. Yeah, I should probably just give in and use a GPS.

I reached Shippensburg Road at mile 57, the start of the Big Flat climb, and was looking for Mike who said he'd be there with water, but I was so slow he was already gone. Fortunately I had plenty of Gatorade left. I went over the first false summit (the one that's so early it's obviously not the top), then stopped to pee and take some Ibuprofin (knees not too bad, but lower back sore), then climbed forever to the second false summit (the one that really fools you the first time you do this climb), then took a brief rest stop in the shade, then finished the climb. It took forever but my legs and lungs were both fine since I did the climb at an easy pace. Only my lower back hurt, and it felt better as soon as the climbing stopped, so not a problem. But my left hand started cramping on the descent from (probably excessive) braking, so I took one more stop to rest it a bit, before continuing into Shippensburg.

One of the pre-riders had said the McDonalds was really slow, so I went to Sheetz instead. There was a long line for "real" food so I just grabbed some Gatorade and a Klondike bar, and ate a Clif Bar from my bag. The quick control probably got me back 10 minutes of the hour I lost going over Big Flat at 3 mph. But, hey, over a third of the ride done, along with the two worst climbs. Now I could use all the energy I had left to ride the rest of the way at decent speed and finish in a reasonable time.

The next 10 miles or so went okay, but then I got to Creek Road. I tend to like roads with "Creek" in their name because they're flat and have a nice view of water. This one was hilly and had no water anywhere, at least for the first several miles, before it finally flattened out and the creek appeared. I propose renaming the first section to "Crappy View Rollers Road."

I stopped at the Unimart at mile 92 and saw one other rider, but he wasn't with our group. I was hot so I had ice cream again, which was probably a mistake. Crossed the 100-mile mark around 1 p.m., for a very unimpressive 8-hour century, but at least most of the hills were past. I got confused by the cue sheet and signs at one of the unmarked Yellow Breeches Road turns, and spent a minute trying to get my cell phone GPS to help then realizing it didn't have a local map loaded or any signal to fetch one so all it could do was put a blue dot in the middle of Pennsylvania. So I picked a direction at random and it ended up working out.

The third-biggest climb of the ride was Whiskey Springs around mile 110. I didn't even remember it being hard on previous rides, just that I dropped my chain shifting down for it once, but I'm in much worse shape now so it was slow going. But I was happy to have the day's four worst climbs behind me. Now just an easy downhill coast to the finish. Well, not quite.

I got to Rocco's Pizza in East Berlin around 5 p.m. 12 hours to do 200k, so an 18-hour pace, but all the hard climbing was behind me so maybe I could pick it up. There were no other riders there, and none arrived while I was there. I got two slices of pizza (probably a mistake), a Pepsi, and a pitcher of water to refill my bottles. After a bit of lollygagging I headed back out, then got confused at the PA 194 turn (the cue sheet said Abbottstown St. but the street sign said something else) and wasted a couple of minutes making sure. There was a fair bit of traffic on 194 but it was pretty polite. I was trying to cover as much ground as possible before dark, but my legs weren't hearing it, and I was doing a lot of 12 mph plodding. Not bonked or dehydrated, just fatigued.

Around mile 150 it started getting a bit dark and a bit cold so I put on my reflective vest and turned on one taillight. Around mile 160 the sun actually went down so I turned on all my lights. The dark definitely complicated navigation and it seemed like it took forever to get to Thurmont at mile 167. I stopped at Roy Rogers and had a roast beef sandwich, because I didn't want to bonk at the end. But a Clif Bar from my bag would have worked just as well, so it was really just an excuse to get off the bike for a few minutes. I put on my long-sleeved jersey, but never bothered with my tights, let alone the other winter gear that sat in my bag all day. (It was still over 50.)

The last 20 miles of the ride were really slow and featured a lot of stopping to check street signs and make sure I was going the right way, plus a lot of angst about missing the turn onto Blacks Mill Road (labeled "Easy to Miss / sign askew"). My speed was down to around 10 mph by the time I reached Frederick, then I got a bit of a second wind and rode at maybe 13 mph for the last couple of miles. No injuries, bonk, or dehydration, just pure fatigue. I did have enough energy to drive home, but I almost missed the turn onto 15S that I've taken a hundred times, so I probably shouldn't have. My usual policy is to get a hotel room after 400s, but I'm going to change that to also get a room after 300s until my 300 times stop looking like 400 times.

I finished second-to-last out of 39 finishers, not counting the pre-riders. I know exactly how to fix this: lose 30 pounds (good for 2.5 mph using Grant Petersen's rule of thumb), keep bike commuting every day to strengthen my knees (which have not been a problem this year, like they were the last two years when I wasn't commuting), and keep riding brevets to build endurance. Unfortunately it's too late for this spring brevet season. But I hope to be back in the midpack by next year.

Bicycles

Comments (0)

Permalink

DC Randonneurs Warrenton 300 km Brevet Pre-Ride

I volunteered to organize the Warrenton 300, which meant I also got to do the pre-ride.

Unfortunately, due to an unusually cold, snowy, and late winter, our rides keep getting postponed this winter and "spring." The 100 km populaire that was supposed to happen on January 17 got moved back a week to January 24, and it was cold enough that water bottles froze. The Pastries and Coffee 200 that was supposed to happen on March 7 actually happened on March 14. And the Urbana 200 got pushed back from March 21 to March 28, conflicting with the Warrenton 300 pre-ride. The net result of all this for me was that I ended up riding a 300 without first riding a 200 in 2015. Not recommended.

I went looking for someone to do the pre-ride with me, not really expecting to find any takers since it conflicted with the Urbana 200, but Charlie stepped up. Good thing; I probably wouldn't have finished without him. Thanks Charlie.

The weather forecast was pretty dire. Temperatures in the 30s all day, with a small chance of snow at the start. And sustained 15 mph winds out of the northwest all day. The actual weather was a bit worse than the forecast — it got down to 23 by the end of the ride, and the winds got up to 20 sustained, 25 gusts. The only good thing about the forecast was that since it was supposed to be cold all day, we didn't have to mess around with removing and stowing clothes as it warmed up, putting on sunscreen, etc.

Charlie and I shared a room at the Hampton Inn. I didn't sleep well, and got up at 4:15 needing 3 or 4 more hours of sleep. I was ready to go at 4:45, after having a Clif Bar for breakfast, but Charlie took longer, and we didn't actually roll out until 5:07. The light at 211 that doesn't usually change for even large groups of bikes changed for us right away, before I even hit the pedestrian button — maybe they fixed the sensor. (I recommend that someone hit the button anyway.) I led us through sleepy Warrenton and up to route 55, then Charlie pulled for a while. It was cold, and there was a headwind already, but we had lots of energy early. It was a bit below freezing, and we saw a few thin patches of black ice, but never actually hit one and had a problem.

By the time we hit the rollers on 55, Charlie was complaining about cold hands, a complaint that would continue all day. He was wearing new Gore Windstopper gloves, and apparently they didn't work as advertised. Once again, never try anything for the first time on a long brevet. I had some old Garneau lobster claws, which are pretty good in the 30s, but I really should have brought my heavy ski mittens that are good in much colder weather. Never overly trust the weather forecast — always think about the worst case. Clothes aren't that heavy.

We controlled (slowly because Charlie needed to warm up) at the 7-11 in Marshall. I had some Cherry Greek yogurt for second breakfast.

After passing through Marshall, the sun come up (all at once), and we rode down beautiful and mildly hilly Crest Hill Road for a while. It was quickly apparent that I couldn't climb with Charlie — he's a lot lighter than me and has more miles in his legs this year. I didn't want to burn energy early in the ride climbing hard, so I just accepted this and fell behind on the climbs, then tried to catch up on the downhills and flats.

In Flint Hill we stopped for a minute so I could take notes on some bit potholes and ruts we'd dodged. The coffee shop was supposed to open at 8:30, but it was 8:30 and it was still closed, so no coffee. We continued to (Little) Washington and Charlie went into a coffee shop there to warm up his hands.

As we moved toward Etlan it started to warm up a bit. Above freezing, anyway. The wind was still brutal, but as the route turned south it became a side wind rather than a block headwind. Charlie dropped me on the climb over the shoulder of Old Rag, but it's a pretty short climb from that direction and I caught back up at the bottom of the hill, after taking the corkscrew descent very carefully. (Good thing, because there was a truck coming around one of the blind curves, taking up 2/3 of the road, and if I hadn't been all the way to the right under complete control it could have been ugly. Don't mess around with blind descents.) My bad knee started to hurt around mile 60, so I took a couple of Ibuprofin and told it to shut up. I had to take a couple more doses later in the day, but my knee never got bad enough to be a real problem. More evidence that that lots of short commutes are good for my knee — I'm happy to be back to bike commuting this year.

The climb up the edge of Old Rag was the highest point of the day, and half the ride's climbing is in the first third of the ride, so in theory once you get there you're home free. In practice, between my lack of recent distance riding and Charlie's lack of sufficient cold-weather gear, we didn't go much faster after that.

We controlled at Syria (pronounced sigh-REE-uh, not like the country in the middle east), and once again spent way too long at a control to warm up. I had one of their ham biscuits, which was okay. The next section of the ride, down Blue Ridge Turnpike / Hoover / Hebron Valley / Hebron Church, is one of my favorites. Gently rolling, very pretty, not too much traffic (except when there's an apple festival). We got passed by a big tractor holding a bale of hay in a giant rear-mounted claw; looked like something out of Transformers (without the cheesy explosions). For a few minutes the wind briefly became a tailwind, everything went silent, and we effortlessly went 22 mph. Then we turned and it was back to a side wind.

We had our first navigational incident of the day at mile 85 when the sign for Good Hope Church Road was down and Charlie took the wrong road. The cue sheet said it was a sharp turn, though, and there was a sign for the Good Hope Church, and then we found the actual Good Hope Church Road sign, so we eventually figured it out.

We stopped at the Hardee's in Gordonsville around mile 100 for lunch. We needed the calories and the break, but again I think we spent too long in the control. The temperature was up to 35 by then, and the wind died down a bit for a while, but then resumed blowing a few minutes after we left.

At mile 105, two vicious but squirrel-sized dogs charged into our path, on a mission to get tangled up in our spokes. They were surprisingly fast for such short-legged things (plus we were not exactly burning up the roads) and nipped at our heels for maybe a quarter mile before giving up. And then right while we were distracted by the dogs, a giant pothole appeared, and almost swallowed my front wheel. I'm not saying those dogs deliberately led us into that hole, but I'm not saying they didn't, either. Fortunately that was our only dog alert on the day. (We did see 3 separate groups of deer in or near the road, and one early-morning fox flashing in front of us on a downhill, but none came close enough to be a serious danger.)

We stopped again at the Subway in Orange for dinner around 5:45. Once again, we spent too long in a control. Also, the Subway is a bit off the route, so going there probably cost us a bonus mile. But I had a pretty good sub and an excellent Macadamia Nut cookie. The extra distance threw off my navigation, so I wasn't sure when to look for Clarks Mountain Road, but we eventually got there. After a brief after-dinner burst of energy wore off, I had a hard time keeping near Charlie, and fell way behind on the climbs, then didn't really feel up to pedalling on the downhills.

My goal was to at least get off 522 (known for high-speed traffic, psychotic drivers, and no shoulders) before it got dark. Fortunately, traffic was a bit lighter than usual (maybe because it was dinner time?), and there was only one crazy pickup-truck driver this time, almost causing a head-on collision to avoid waiting a few seconds for oncoming traffic to clear before passing us. We made it to Bakers Store, verified their closing time, and then quickly got back on our bikes and got off onto nicer roads.

Algonquin Trail – Batna – Lignum is another favorite part of the ride for me, but it was getting dark and colder, so I didn't get to enjoy it much. (For comparison, last year I finished the ride just as it got dark. So about 40 miles ahead of this year's pace.) Charlie said he might not be able to make it to the Citgo to put in new shoe warmers, so I suggested we try the post office in Lignum, right before Route 3. In was unlocked, so he had a chance to warm up a bit and put them in, but that they really didn't work that well. He was wearing summer cycling shoes and winter overbooties with one pair of socks, not quite enough. My feet (in Lake winter boots and two pairs of socks) were fine almost to the end of the ride, when they started getting a little chilly.

Eleys Ford and Edwards Shop were kind of rough, and it was hard to see the holes and ruts in the dark, and there was a surprising amount of traffic for these usually quiet roads. But we made it to the bridge over the Rappahannock without crashing, and then up to Remington. Charlie was now pulling away from me even on the flats, where I had problems maintaining even 11 mph. I think it was just fatigue, not bonk since I'd eaten a lot, though I may have been a bit dehydrated since I was having a hard time forcing down enough liquid. A train came down the tracks in Remington, forcing Charlie to stop and letting me catch up as we got into the Citgo a few minutes before it closed at 10. My taillights were getting a bit dim, so I swapped in my spare batteries.

The last 20 miles of the ride are kind of a blur. Charlie led, and I basically just followed his taillight in the distance rather than trying to navigate for myself. I was capable of maybe 10 mph on the flat, in my small ring. Fortunately the wind was dying down. Unfortunately it was getting colder, and my hands were getting numb enough that shifting was difficult. We missed the split at mile 181, but Charlie's GPS showed us off-track about a quarter mile later, so we only lost about half a mile. (His GPS battery is only supposed to be good for about 14 hours but made it to the end. Maybe the cold helped it?) And then I missed the Frytown turn at mile 185, with Charlie's taillight out of sight. While I was sitting, reading my cue sheet and trying to figure out where I messed up, I heard Charlie's voice. He'd seen me miss the turn and chased me down (not so hard at my speed) to drag me back on course. So more bonus miles for me and even more for him.

After that I tried to stay closer, and succeeded until we were on Walker Drive and I knew I couldn't miss the last turn because it was at a light. We finished at 12:18, for a 19:18 time, out of a maximum of 20 hours. Last year I was 14:59 and Charlie was 15:08 (though he took the gravel detours which are slower than the paved roads). So, over 4 hours slower, due to a combination of cold and wind and (in my case) lack of fitness.

We made it, but there was some luck involved. Charlie was really cold all day, which could have led to real problems. I was really tired and beat at the end, which could have led to a crash.

Lessons: Bring clothing for the worst case, not the forecast. If the forecast says 30, be prepared for 15. Gradually increase distance early in the year, rather than trying to jump from commute distance to 300k. Get through controls faster. Test new equipment on short rides; don't change anything right before a long ride.

Hoping for much warmer weather for next week's ride. It's a really nice course, if the conditions are reasonable. Not too hilly, not too much traffic except in a couple of spots.

Bicycles

Comments (0)

Permalink

PyPy 2.5 Speed Test

I saw there was a new version of PyPy available, and realized I hadn't benchmarked it in a while. So here's a quick test of CPython 2.7 versus PyPy 2.2.1 (the default on Ubuntu 14.04 LTS) and PyPy 2.5 (the latest and greatest, unless you're reading this post in the future).

The benchmark is simply running all of my Python Project Euler solutions on each of these Python versions, throwing away any that didn't work or took longer than 60 seconds on any of them, and then dumping all the runtimes. There is definitely some random noise in the numbers, but it should mostly even out over many programs. (There were 145 that made the time cut this time.)

I ran these on Ubuntu 14.04 on an Amazon EC2 c4.2xlarge instance. (A Haswell Xeon CPU running somewhere in the 2.9 – 3.2 GHz range depending on whether Turbo Mode is engaged, 4 or 8 cores depending on whether you count HyperThreading, 15 GB.)

I'll give the results first in case you don't want to scroll past the big table: PyPy 2.5 177.35s, PyPy 2.2.1 208.11s, CPython 2.7 616.73s. So PyPy 2.5 is about 17% faster than PyPy 2.2.1 on this benchmark, a nice sign that PyPy keeps getting faster even though much of the low-hanging fruit was picked long ago. And PyPy is about 3.5 times as fast, or 250% faster, than CPython 2.7 on this benchmark.

I don't see any interesting cases where CPython significantly beats PyPy. We used to see cases like that, but as PyPy has matured it's become more consistently fast on a larger set of programs.) I guess the closest to an interesting result is euler225.py, where CPython beats PyPy 23.74s to 27.75s. Not a huge difference, but worth taking a look. I may try to figure out what's going on and post a followup.

script PyPy 2.2.1 PyPy 2.5 CPython 2.7.6
euler1.py 0.31 0.10 0.31
euler2.py 0.31 0.10 0.10
euler3.py 0.20 0.10 0.10
euler4.py 0.10 0.10 0.10
euler5.py 0.10 0.10 0.10
euler6.py 0.10 0.10 0.10
euler7.py 0.10 0.10 0.20
euler8.py 0.10 0.10 0.10
euler9.py 0.10 0.10 0.10
euler10.py 0.99 0.71 3.20
euler11.py 0.10 0.10 0.10
euler13.py 0.10 0.10 0.10
euler14.py 1.50 0.90 1.50
euler15.py 0.10 0.10 0.10
euler16.py 0.10 0.10 0.10
euler18.py 0.10 0.10 0.10
euler19.py 0.10 0.10 0.10
euler20.py 0.10 0.10 0.10
euler21.py 0.10 0.10 0.10
euler22.py 0.10 0.10 0.10
euler23.py 0.60 0.60 4.91
euler24.py 1.60 1.00 2.11
euler25.py 0.20 0.10 0.10
euler26.py 1.71 1.10 1.91
euler27.py 1.10 0.60 4.71
euler28.py 0.10 0.10 0.10
euler29.py 0.10 0.10 0.10
euler30.py 0.70 0.30 2.21
euler32.py 0.60 0.50 1.30
euler33.py 0.10 0.10 0.10
euler34.py 1.50 1.00 4.91
euler35.py 2.31 1.20 10.42
euler36.py 0.30 0.30 0.90
euler37.py 2.41 1.70 5.01
euler38.py 0.40 0.30 0.50
euler39.py 0.10 0.10 0.10
euler40.py 0.20 0.20 0.30
euler41.py 1.70 0.60 1.60
euler42.py 0.10 0.10 0.10
euler43.py 5.91 4.31 11.52
euler44.py 0.30 0.30 1.10
euler45.py 0.40 0.40 0.60
euler46.py 0.20 0.10 0.40
euler47.py 0.40 0.30 1.20
euler48.py 0.10 0.10 0.10
euler49.py 0.20 0.10 0.20
euler50.py 0.90 0.70 27.36
euler51.py 4.41 2.61 9.72
euler52.py 0.20 0.10 0.30
euler53.py 0.10 0.10 0.10
euler54.py 0.20 0.20 0.10
euler55.py 0.30 0.10 0.10
euler56.py 0.20 0.10 0.30
euler57.py 0.10 0.10 0.30
euler58.py 2.61 2.41 28.96
euler59.py 2.21 1.20 9.72
euler61.py 0.20 0.10 0.10
euler62.py 0.20 0.10 0.10
euler63.py 0.10 0.10 0.10
euler64.py 3.11 2.91 38.38
euler65.py 0.10 0.10 0.10
euler66.py 0.70 0.70 5.71
euler67.py 0.10 0.10 0.10
euler68.py 0.10 0.10 0.10
euler69.py 0.10 0.10 0.10
euler70.py 0.20 0.20 0.30
euler71.py 0.10 0.10 0.40
euler72.py 3.51 2.91 21.84
euler73.py 1.90 1.71 16.23
euler75.py 7.12 0.60 1.00
euler77.py 0.20 0.30 0.20
euler79.py 0.10 0.10 0.10
euler80.py 0.10 0.10 0.20
euler81.py 0.10 0.10 0.10
euler82.py 0.20 0.20 0.20
euler83.py 0.20 0.10 0.40
euler84.py 0.90 0.80 13.83
euler85.py 0.90 1.00 5.11
euler87.py 0.20 0.30 0.40
euler88.py 3.31 1.40 2.21
euler89.py 0.10 0.10 0.10
euler92.py 13.93 7.72 46.80
euler93.py 2.11 1.20 4.01
euler94.py 4.31 3.41 5.71
euler95.py 11.62 13.33 53.92
euler97.py 0.20 0.20 0.80
euler98.py 0.20 0.20 0.20
euler99.py 0.10 0.10 0.10
euler100.py 0.10 0.10 0.10
euler101.py 0.20 0.20 0.10
euler102.py 0.10 0.10 0.10
euler103.py 0.10 0.10 0.10
euler104.py 0.30 0.30 0.60
euler105.py 0.80 0.20 0.20
euler106.py 0.80 0.20 0.20
euler107.py 0.20 0.20 0.10
euler108.py 0.50 0.50 2.81
euler109.py 0.30 0.20 1.30
euler111.py 0.80 0.30 7.02
euler112.py 2.01 0.80 4.81
euler114.py 0.10 0.10 0.10
euler115.py 0.10 0.10 0.20
euler116.py 0.10 0.10 0.10
euler117.py 0.10 0.10 0.10
euler119.py 0.10 0.10 0.10
euler120.py 0.10 0.10 0.10
euler121.py 0.10 0.10 0.10
euler122.py 13.63 13.73 18.34
euler123.py 1.10 1.11 2.21
euler124.py 0.40 0.40 0.90
euler125.py 0.20 0.30 0.40
euler126.py 0.70 0.80 5.01
euler135.py 0.20 0.20 0.60
euler136.py 8.12 6.31 36.97
euler142.py 0.10 0.10 0.10
euler143.py 0.10 0.10 0.10
euler147.py 0.10 0.10 0.30
euler149.py 9.22 7.62 17.34
euler150.py 0.10 0.10 0.20
euler162.py 0.10 0.10 0.10
euler171.py 0.10 0.10 0.10
euler172.py 0.30 0.30 0.20
euler173.py 0.10 0.10 0.30
euler174.py 0.50 0.70 0.80
euler181.py 0.10 0.10 0.10
euler188.py 0.20 0.10 0.10
euler190.py 0.10 0.10 0.10
euler202.py 0.10 0.10 0.10
euler205.py 0.40 0.40 0.30
euler206.py 4.51 4.21 15.63
euler207.py 0.20 0.20 0.20
euler222.py 0.10 0.10 0.10
euler225.py 27.75 27.75 23.74
euler227.py 0.10 0.10 0.60
euler230.py 0.10 0.10 0.10
euler233.py 0.10 0.10 0.10
euler234.py 1.30 1.00 2.51
euler235.py 0.10 0.10 0.10
euler240.py 3.41 1.91 7.22
euler267.py 0.20 0.30 0.20
euler286.py 9.02 8.12 54.39
euler345.py 15.13 21.54 27.76
euler346.py 1.00 0.80 1.80
euler347.py 6.41 6.92 20.04
euler371.py 0.10 0.10 0.10
total 208.11 177.35 616.73
wins 87 127 71

Programming
Python

Comments (0)

Permalink

Why is Go slower than Python for this parallel math code?

I was recently messing around with one of my old Project Euler solutions (specifically, problem 215) to test PyPy's new software transactional memory feature, and decided to port it to Go to see how the code compared.

The first question I had was how to do generators in Go. Python has the magic yield statement, so you can do things like this:

def gen_ways(width, blocks):
    """Generate all possible permutations of items in blocks that add up
    to width, as strings."""
    if width == 0:
        yield ""
    else:
        for block in blocks:
            if block <= width:
                for way in gen_ways(width - block, blocks):
                    yield str(block) + way

The Go equivalent is a function that returns an output-only unbuffered channel, which returns the output from a goroutine. It's not quite as terse, but then it didn't require adding extra support to the language, either.

/* Generate all possible permutations of items in blocks that add up to width,
as strings. */
func gen_ways(width int, blocks []int) chan string {
    out := make(chan string)
    go func() {
        if width == 0 {
            out <- ""
        } else {
            for _, block := range blocks {
                if block <= width {
                    for way := range gen_ways(width-block, blocks) {
                        out <- strconv.Itoa(block) + way
                    }
                }
            }
        }
        close(out)
    }()
    return out
}

Longer, but it's very nice to be able to do this without an additional keyword. One gripe: the channel is logically output-only, but the code doesn't work unless I make it bidirectional. (Because the function is recursive?)

The next function to port looked like this:

def build_mirrors(ways):
    mirrored = set()
    mirrors = set()
    for way in ways:
        rev = "".join(reversed(way))
        if way != rev:
            low, high = sorted([way, rev])
            mirrored.add(low)
            mirrors.add(high)
    return mirrored, mirrors

Go lacks a builtin set. I considered just building my own with a map, but instead pulled in github.com/deckarep/golang-set. Which worked okay. One annoyance was that I couldn't just declare a set and have a useful set; I had to call mapset.NewSet(). Can't really fault the author of the set module for that problem, though, since Go's builtin maps and arrays and chans have the same problem. Zero values that give runtime errors when you try to use them aren't very useful. So much for static typing helping find errors at compile time. Anyway, I ended up with this:

func build_mirrors(ways []string) (mirrored, mirrors mapset.Set) {
    mirrored = mapset.NewSet()
    mirrors = mapset.NewSet()
    for _, way := range ways {
        rev := reverse(way)
        if way < rev {
            mirrored.Add(way)
            mirrors.Add(rev)
        } else if rev < way {
            mirrored.Add(rev)
            mirrors.Add(way)
        }
    }
    return
}

The next concept I had to translate was a group of processes or threads running in parallel, reading from a common input queue and writing to a common results queue. Here's the Python function:

def count_combos(in_queue, out_queue, compatible_ways):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, count the number of results, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            count += 1
        out_queue.put((height, prev_way, count))

and the blob of code that calls it:

in_queue = multiprocessing.Queue()
out_queue = multiprocessing.Queue()
cpus = multiprocessing.cpu_count()

half = (height - 1) // 2
for way in ways:
    if way not in mirrors:
        in_queue.put((half, way))
# sentinels
for unused in xrange(cpus):
    in_queue.put((None, None))
procs = []
for unused in xrange(cpus):
    proc = multiprocessing.Process(target=count_combos,
                                   args=(in_queue,
                                         out_queue,
                                         compatible_ways))
    proc.daemon = True
    proc.start()
    procs.append(proc)

I think the called code is fine, but the calling code is kind of ugly, because of the way I need to manually track the process objects to clean them up later. Also, it might have been better with more explicit sentinels than just lazily using None.

The Go version of the called code is similar, except that I had to setup some structs rather than just passing tuples around.

type (
    height_way struct {
        height int
        way    string
    }
    height_way_count struct {
        height int
        way    string
        count  uint64
    }
)

const SENTINEL_HEIGHT = -1

/* Read height_way structs from in_queue, call gen_combos
   on each, count the number of results, and put height_way_count
   structs on out_queue. */
func count_combos(in_queue <-chan height_way,
    out_queue chan<- height_way_count,
    compatible_ways map[string][]string) {
    for {
        hw := <-in_queue
        height := hw.height
        way := hw.way
        if height == SENTINEL_HEIGHT {
            return
        }
        var count uint64
        for _ = range gen_combos(height, way, compatible_ways) {
            count += 1
        }
        out_queue <- height_way_count{height, way, count}
    }
}

The Go version of the calling code is nicer, because goroutines are builtin syntax so you don't need to do manual process management.

    const QUEUE_SIZE = 9999

    in_queue := make(chan height_way, QUEUE_SIZE)
    out_queue := make(chan height_way_count, QUEUE_SIZE)

    half := (height - 1) / 2
    for _, way := range ways {
        if !mirrors.Contains(way) {
            in_queue <- height_way{half, way}
        }
    }
    for ii := 0; ii < maxprocs; ii++ {
        in_queue <- height_way{SENTINEL_HEIGHT, ""}
    }
    for ii := 0; ii < maxprocs; ii++ {
        go count_combos(in_queue, out_queue, compatible_ways)
    }

It was surprisingly tricky to make this work without deadlocking. First I forgot to set the channel size, which meant I had blocking channels, which immediately blocked when I put data on them, since the goroutine to receive the data wasn't running yet. Unbuffered and buffered channels are fundamentally different beasts.

Finally, when I got everything working, the Go program only ran on a single CPU core. I remembered seeing GOMAXPROCS in the docs, and doing "GOMAXPROCS=4 ./euler215" worked. But when I tried setting it from inside the program, like "runtime.GOMAXPROCS = 4", the variable changed but the program never actually used multiple cores. So runtime.GOMAXPROCS is an attractive nuisance; it's documented, and looks like it should work, but (at least in Go 1.2.1 on Ubuntu) doesn't really. (This may be issue 1492 , but that bug is listed as fixed.)

Anyway, once the program gave the correct answer on the trivial problem size (width 9, height 3), it was time to run it with the full problem size (width 32, height 10). On my (slow) 3 GHz Phenom II quad-core, CPython 2.7 takes 21:13 and PyPy 2.3.1 takes 7:06, so I was figuring the Go version should finish in a couple of minutes.

Nope. Half an hour later, I was wondering what was wrong. An hour later, I was still wondering. The program did finish eventually, and gave the right answer (once I changed a few counters from int to uint64 to keep them from rolling over), but it took 81 minutes. Almost 4 times as slow as CPython, and almost 12 times as slow as PyPy.

So I instrumented the Go version for profiling, following the directions from the Go blog. After my program was profiled, I ran it with "time GOMAXPROCS=4 ./euler215 -cpuprofile prof.out -width 25 -height 10" so it would run fairly quickly, then ran "go tool pprof ./euler215 prof.out" to enter the interactive profiler, then "top25" to see:

(pprof) top25
Total: 2883 samples
     226   7.8%   7.8%      226   7.8% scanblock
     221   7.7%  15.5%      221   7.7% etext
     218   7.6%  23.1%      739  25.6% runtime.mallocgc
     195   6.8%  29.8%      195   6.8% runtime.xchg
     185   6.4%  36.2%      191   6.6% runtime.settype_flush
     181   6.3%  42.5%     1631  56.6% main.func·002
     173   6.0%  48.5%      173   6.0% sweepspan
     146   5.1%  53.6%      146   5.1% runtime.casp
     112   3.9%  57.5%      208   7.2% runtime.markallocated
     108   3.7%  61.2%      770  26.7% cnew
      98   3.4%  64.6%       98   3.4% markonly
      95   3.3%  67.9%      692  24.0% runtime.growslice
      92   3.2%  71.1%       92   3.2% runtime.memclr
      70   2.4%  73.5%      330  11.4% runtime.makeslice
      53   1.8%  75.4%      101   3.5% runtime.unlock
      51   1.8%  77.1%      166   5.8% runtime.chansend
      45   1.6%  78.7%      122   4.2% runtime.lock
      43   1.5%  80.2%      159   5.5% runtime.chanrecv
      35   1.2%  81.4%      597  20.7% growslice1
      31   1.1%  82.5%       31   1.1% runtime.memmove
      29   1.0%  83.5%       29   1.0% schedule
      25   0.9%  84.4%       25   0.9% park0
      24   0.8%  85.2%       24   0.8% flushptrbuf
      23   0.8%  86.0%       43   1.5% runtime.mapaccess1_faststr
      17   0.6%  86.6%       17   0.6% dequeue

Meh. It's spending a lot of time making slices. Growing slices. Clearing memory. Allocating memory. Sending and receiving on channels. Basically, all down inside the Go runtime, not in my code. Not a whole lot of useful information on what to fix. Maybe I'm doing something incorrect with maps that's causing excessive memory churn. Or maybe Go maps are just too slow.

Anyway, I'll dump the full code here, in case anyone wants to see the full example. Here's the Python program:

#!/usr/bin/env python

"""Project Euler, problem 215

Consider the problem of building a wall out of 2x1 and 3x1 bricks
(horizontal vertical dimensions) such that, for extra strength, the gaps
between horizontally adjacent bricks never line up in consecutive layers, i.e.
never form a "running crack".

For example, the following 9x3 wall is not acceptable due to the running
crack shown in red:

3222
2232
333

There are eight ways of forming a crack-free 9x3 wall, written W(9,3) = 8.

Calculate W(32,10).
"""

import sys
import multiprocessing

try:
    import psyco
    psyco.full()
except ImportError:
    pass


sys.setrecursionlimit(100)


def gen_ways(width, blocks):
    """Generate all possible permutations of items in blocks that add up
    to width, as strings."""
    if width == 0:
        yield ""
    else:
        for block in blocks:
            if block <= width:
                for way in gen_ways(width - block, blocks):
                    yield str(block) + way


def build_mirrors(ways):
    mirrored = set()
    mirrors = set()
    for way in ways:
        rev = "".join(reversed(way))
        if way != rev:
            low, high = sorted([way, rev])
            mirrored.add(low)
            mirrors.add(high)
    return mirrored, mirrors


def find_cracks(way):
    """Return the set of indexes where cracks occur in way"""
    result = set()
    total = 0
    for ch in way[:-1]:
        total += int(ch)
        result.add(total)
    return result


def crack_free(tup1, tup2, cracks):
    """Return True iff tup1 and tup2 can be adjacent without making a
    crack."""
    return not cracks[tup1].intersection(cracks[tup2])


def find_compatible_ways(way, ways, cracks):
    """Return a list of crack-free adjacent ways for way"""
    result = []
    for way2 in ways:
        if crack_free(way, way2, cracks):
            result.append(way2)
    return result


def build_compatible_ways(ways):
    cracks = {}
    for way in ways:
        cracks[way] = find_cracks(way)
    print "done generating %d cracks" % len(cracks)
    compatible_ways = {}
    compatible_ways[()] = ways
    for way in ways:
        compatible_ways[way] = find_compatible_ways(way, ways, cracks)
    return compatible_ways


def gen_combos(height, prev_way, compatible_ways):
    """Generate all ways to make a crack-free wall of size (width, height),
    as height-lists of width-strings."""
    if height == 0:
        return
    elif height == 1:
        for way in compatible_ways[prev_way]:
            yield [way]
    else:
        for way in compatible_ways[prev_way]:
            for combo in gen_combos(height - 1, way, compatible_ways):
                yield [way] + combo


def count_combos(in_queue, out_queue, compatible_ways):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, count the number of results, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            count += 1
        out_queue.put((height, prev_way, count))


def count_combos_memo(in_queue, out_queue, compatible_ways, memo):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, chain the result of memo to the last result in the combo
    to get the total count, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            last = combo[-1]
            count += memo[last]
        out_queue.put((height, prev_way, count))


def W(width, height):
    """Return the number of ways to make a crack-free wall of size
    (width, height)."""
    ways = sorted(gen_ways(width, [2, 3]))
    print "done generating %d ways" % len(ways)

    mirrored, mirrors = build_mirrors(ways)
    print "have %d mirror images " % (len(mirrored))

    compatible_ways = build_compatible_ways(ways)
    print "done generating %d compatible_ways" % sum(map(
        len, compatible_ways.itervalues()))

    in_queue = multiprocessing.Queue()
    out_queue = multiprocessing.Queue()

    cpus = multiprocessing.cpu_count()

    half = (height - 1) // 2
    for way in ways:
        if way not in mirrors:
            in_queue.put((half, way))
    # sentinels
    for unused in xrange(cpus):
        in_queue.put((None, None))
    procs = []
    for unused in xrange(cpus):
        proc = multiprocessing.Process(target=count_combos,
                                       args=(in_queue,
                                             out_queue,
                                             compatible_ways))
        proc.daemon = True
        proc.start()
        procs.append(proc)

    half_memo = {}

    num_ways = len(ways) - len(mirrors)
    for ii in xrange(num_ways):
        (unused, prev_way, count) = out_queue.get()
        half_memo[prev_way] = count
        if prev_way in mirrored:
            half_memo["".join(reversed(prev_way))] = count
        print "(%d/%d) %s mirrored=%d count=%d" % (
            ii + 1, num_ways, prev_way, prev_way in mirrored, count)

    for proc in procs:
        proc.join()

    rest = (height - 1) - half

    for way in ways:
        if way not in mirrors:
            in_queue.put((rest, way))
    # sentinels
    for unused in xrange(cpus):
        in_queue.put((None, None))
    procs = []
    for unused in xrange(cpus):
        proc = multiprocessing.Process(target=count_combos_memo, args=(
                                       in_queue, out_queue, compatible_ways,
                                       half_memo))
        proc.daemon = True
        proc.start()
        procs.append(proc)

    total = 0
    for ii in xrange(num_ways):
        (unused, prev_way, count) = out_queue.get()
        if prev_way in mirrored:
            count *= 2
        total += count
        print "(%d/%d) %s mirrored=%d count=%d total=%d" % (
            ii + 1, num_ways, prev_way, prev_way in mirrored, count, total)
    for proc in procs:
        proc.join()
    return total


def main():
    try:
        width = int(sys.argv[1])
    except IndexError:
        width = 32
    try:
        height = int(sys.argv[2])
    except IndexError:
        height = 10
    print W(width, height)


if __name__ == "__main__":
    main()

And here's the Go version:

/* Project Euler, problem 215

Consider the problem of building a wall out of 2x1 and 3x1 bricks
(horizontal vertical dimensions) such that, for extra strength, the gaps
between horizontally adjacent bricks never line up in consecutive layers, i.e.
never form a "running crack".

For example, the following 9x3 wall is not acceptable due to the running
crack shown in red:

3222
2232
333

There are eight ways of forming a crack-free 9x3 wall, written W(9,3) = 8.

Calculate W(32,10).
*/

package main

import (
    "fmt"
    "github.com/deckarep/golang-set"
    "os"
    "runtime"
    "strconv"
    "flag"
    "log"
    "runtime/pprof"
)

type (
    height_way struct {
        height int
        way    string
    }
    height_way_count struct {
        height int
        way    string
        count  uint64
    }
)

const SENTINEL_HEIGHT = -1
const QUEUE_SIZE = 9999

/* Generate all possible permutations of items in blocks that add up to width,
as strings. */
func gen_ways(width int, blocks []int) chan string {
    out := make(chan string)
    go func() {
        if width == 0 {
            out <- ""
        } else {
            for _, block := range blocks {
                if block <= width {
                    for way := range gen_ways(width-block, blocks) {
                        out <- strconv.Itoa(block) + way
                    }
                }
            }
        }
        close(out)
    }()
    return out
}

func reverse(str string) (result string) {
    for _, ch := range str {
        result = string(ch) + result
    }
    return
}

func build_mirrors(ways []string) (mirrored, mirrors mapset.Set) {
    mirrored = mapset.NewSet()
    mirrors = mapset.NewSet()
    for _, way := range ways {
        rev := reverse(way)
        if way < rev {
            mirrored.Add(way)
            mirrors.Add(rev)
        } else if rev < way {
            mirrored.Add(rev)
            mirrors.Add(way)
        }
    }
    return
}

/* Return the set of indexes where cracks occur in way */
func find_cracks(way string) (result mapset.Set) {
    result = mapset.NewSet()
    total := 0
    for ii := 0; ii < len(way)-1; ii++ {
        str1 := way[ii : ii+1]
        num, _ := strconv.Atoi(str1)
        total += num
        result.Add(total)
    }
    return result
}

/* Return True iff tup1 and tup2 can be adjacent without making a crack. */
func crack_free(tup1 string, tup2 string, cracks map[string]mapset.Set) bool {
    return cracks[tup1].Intersect(cracks[tup2]).Cardinality() == 0
}

/* Return a list of crack-free adjacent ways for way */
func find_compatible_ways(way string,
    ways []string,
    cracks map[string]mapset.Set) (result []string) {
    for _, way2 := range ways {
        if crack_free(way, way2, cracks) {
            result = append(result, way2)
        }
    }
    return result
}

func build_compatible_ways(ways []string) map[string][]string {
    cracks := make(map[string]mapset.Set)
    for _, way := range ways {
        cracks[way] = find_cracks(way)
    }
    fmt.Printf("done generating %d cracks\n", len(cracks))
    compatible_ways := make(map[string][]string)
    compatible_ways[""] = ways
    for _, way := range ways {
        compatible_ways[way] = find_compatible_ways(way, ways, cracks)
    }
    return compatible_ways
}

/* Generate all ways to make a crack-free wall of size (width, height),
   as height-arrays of width-strings. */
func gen_combos(height int,
    prev_way string,
    compatible_ways map[string][]string) chan []string {
    out := make(chan []string)
    go func() {
        if height == 0 {
            return
        } else if height == 1 {
            for _, way := range compatible_ways[prev_way] {
                wayarray := []string{way}
                out <- wayarray
            }
        } else {
            for _, way := range compatible_ways[prev_way] {
                for combo := range gen_combos(height-1, way, compatible_ways) {
                    wayarray := make([]string, height)
                    wayarray = append(wayarray, way)
                    for _, x := range combo {
                        wayarray = append(wayarray, x)
                    }
                    out <- wayarray
                }
            }
        }
        close(out)
    }()
    return out
}

/* Read height_way structs from in_queue, call gen_combos
   on each, count the number of results, and put height_way_count
   structs on out_queue. */
func count_combos(in_queue <-chan height_way,
    out_queue chan<- height_way_count,
    compatible_ways map[string][]string) {
    for {
        hw := <-in_queue
        height := hw.height
        way := hw.way
        if height == SENTINEL_HEIGHT {
            return
        }
        var count uint64
        for _ = range gen_combos(height, way, compatible_ways) {
            count += 1
        }
        out_queue <- height_way_count{height, way, count}
    }
}

/* Read tuples of (height, prev_way) from in_queue, call gen_combos
   on each, chain the result of memo to the last result in the combo
   to get the total count, and put the count on out_queue. */
func count_combos_memo(in_queue <-chan height_way,
    out_queue chan<- height_way_count,
    compatible_ways map[string][]string,
    memo map[string]uint64) {
    for {
        hw := <-in_queue
        height := hw.height
        way := hw.way
        if height == SENTINEL_HEIGHT {
            return
        }
        var count uint64
        for combo := range gen_combos(height, way, compatible_ways) {
            last := combo[len(combo)-1]
            count += memo[last]
        }
        out_queue <- height_way_count{height, way, count}
    }
}

/* Return the number of ways to make a crack-free wall of size
   (width, height). */
func W(width, height int) uint64 {
    var ways []string
    for way := range gen_ways(width, []int{2, 3}) {
        ways = append(ways, way)
    }
    fmt.Printf("done generating %d ways\n", len(ways))

    mirrored, mirrors := build_mirrors(ways)
    fmt.Printf("have %d mirror images\n", mirrored.Cardinality())

    compatible_ways := build_compatible_ways(ways)
    var total uint64
    for _, way := range compatible_ways {
        total += uint64(len(way))
    }
    fmt.Printf("done generating %d compatible_ways\n", total)

    in_queue := make(chan height_way, QUEUE_SIZE)
    out_queue := make(chan height_way_count, QUEUE_SIZE)

    half := (height - 1) / 2
    for _, way := range ways {
        if !mirrors.Contains(way) {
            in_queue <- height_way{half, way}
        }
    }
    maxprocs := runtime.NumCPU()
    // XXX This doesn't seem to work reliably in Go 1.2.1; set GOMAXPROCS
    // environment variable instead of doing it after the program starts.
    // https://code.google.com/p/go/issues/detail?id=1492 ?
    runtime.GOMAXPROCS(maxprocs)
    for ii := 0; ii < maxprocs; ii++ {
        in_queue <- height_way{SENTINEL_HEIGHT, ""}
    }
    for ii := 0; ii < maxprocs; ii++ {
        go count_combos(in_queue, out_queue, compatible_ways)
    }

    half_memo := make(map[string]uint64)

    num_ways := len(ways) - mirrors.Cardinality()
    fmt.Println("num_ways", num_ways)
    for ii := 0; ii < num_ways; ii++ {
        hwc := <-out_queue
        prev_way := hwc.way
        count := hwc.count
        half_memo[prev_way] = count
        if mirrored.Contains(prev_way) {
            half_memo[reverse(prev_way)] = count
        }
        fmt.Printf("(%d/%d) %s mirrored=%v count=%d\n", ii+1, num_ways,
            prev_way, mirrored.Contains(prev_way), count)
    }

    rest := (height - 1) - half

    in_queue2 := make(chan height_way, QUEUE_SIZE)
    out_queue2 := make(chan height_way_count, QUEUE_SIZE)

    for _, way := range ways {
        if !mirrors.Contains(way) {
            in_queue2 <- height_way{rest, way}
        }
    }
    for ii := 0; ii < maxprocs; ii++ {
        in_queue2 <- height_way{SENTINEL_HEIGHT, ""}
    }
    for ii := 0; ii < maxprocs; ii++ {
        go count_combos_memo(in_queue2, out_queue2, compatible_ways, half_memo)
    }

    var total2 uint64
    for ii := 0; ii < num_ways; ii++ {
        hwc := <-out_queue2
        prev_way := hwc.way
        count := hwc.count
        if mirrored.Contains(prev_way) {
            count *= 2
        }
        total2 += uint64(count)
        fmt.Printf("(%d/%d) %s mirrored=%v count=%d total2=%d\n", ii+1,
            num_ways, prev_way, mirrored.Contains(prev_way), count, total2)
    }
    return total2
}

func main() {
    var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
    var width = flag.Int("width", 32, "width in blocks")
    var height = flag.Int("height", 10, "height in blocks")
    flag.Parse()

    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }

    fmt.Println(W(*width, *height))
}

Programming
Python

Comments (0)

Permalink

A Quick Test of PyPy-STM

The Software Transactional Memory branch of PyPy has been under development for a while now, and the first binary release was made available earlier this month. (See the announcement.) So I went digging through my collection of Project Euler solutions, looking for a good candidate to test with pypy-stm. Basically, I needed a CPU-bound program that used multiple threads, and didn't depend on C libraries that weren't available in PyPy.

I found one, my solution to Project Euler Problem 215. It used the multiprocessing module to run on multiple CPU cores (without tripping over Python's global interpreter lock and ending up with single-core performance). But multiprocessing uses almost the same interface as threading, so it just took a few simple tweaks to switch it over.

Here's the multiprocessing version:

#!/usr/bin/env python

"""Project Euler, problem 215

Consider the problem of building a wall out of 2x1 and 3x1 bricks
(horizontal vertical dimensions) such that, for extra strength, the gaps
between horizontally adjacent bricks never line up in consecutive layers, i.e.
never form a "running crack".

For example, the following 9x3 wall is not acceptable due to the running
crack shown in red:

3222
2232
333

There are eight ways of forming a crack-free 9x3 wall, written W(9,3) = 8.

Calculate W(32,10).
"""

import sys
import multiprocessing


sys.setrecursionlimit(100)


def gen_ways(width, blocks):
    """Generate all possible permutations of items in blocks that add up
    to width, as strings."""
    if width == 0:
        yield ""
    else:
        for block in blocks:
            if block <= width:
                for way in gen_ways(width - block, blocks):
                    yield str(block) + way


def build_mirrors(ways):
    mirrored = set()
    mirrors = set()
    for way in ways:
        rev = "".join(reversed(way))
        if way != rev:
            low, high = sorted([way, rev])
            mirrored.add(low)
            mirrors.add(high)
    return mirrored, mirrors


def find_cracks(way):
    """Return the set of indexes where cracks occur in way"""
    result = set()
    total = 0
    for ch in way[:-1]:
        total += int(ch)
        result.add(total)
    return result


def crack_free(tup1, tup2, cracks):
    """Return True iff tup1 and tup2 can be adjacent without making a
    crack."""
    return not cracks[tup1].intersection(cracks[tup2])


def find_compatible_ways(way, ways, cracks):
    """Return a list of crack-free adjacent ways for way"""
    result = []
    for way2 in ways:
        if crack_free(way, way2, cracks):
            result.append(way2)
    return result


def build_compatible_ways(ways):
    cracks = {}
    for way in ways:
        cracks[way] = find_cracks(way)
    print "done generating %d cracks" % len(cracks)
    compatible_ways = {}
    compatible_ways[()] = ways
    for way in ways:
        compatible_ways[way] = find_compatible_ways(way, ways, cracks)
    return compatible_ways


def gen_combos(height, prev_way, compatible_ways):
    """Generate all ways to make a crack-free wall of size (width, height),
    as height-lists of width-strings."""
    if height == 0:
        return
    elif height == 1:
        for way in compatible_ways[prev_way]:
            yield [way]
    else:
        for way in compatible_ways[prev_way]:
            for combo in gen_combos(height - 1, way, compatible_ways):
                yield [way] + combo


def count_combos(in_queue, out_queue, compatible_ways):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, count the number of results, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            count += 1
        out_queue.put((height, prev_way, count))


def count_combos_memo(in_queue, out_queue, compatible_ways, memo):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, chain the result of memo to the last result in the combo
    to get the total count, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            last = combo[-1]
            count += memo[last]
        out_queue.put((height, prev_way, count))


def W(width, height):
    """Return the number of ways to make a crack-free wall of size
    (width, height)."""
    ways = sorted(gen_ways(width, [2, 3]))
    print "done generating %d ways" % len(ways)

    mirrored, mirrors = build_mirrors(ways)
    print "have %d mirror images " % (len(mirrored))

    compatible_ways = build_compatible_ways(ways)
    print "done generating %d compatible_ways" % sum(map(
        len, compatible_ways.itervalues()))

    in_queue = multiprocessing.Queue()
    out_queue = multiprocessing.Queue()

    cpus = multiprocessing.cpu_count()

    half = (height - 1) // 2
    for way in ways:
        if way not in mirrors:
            in_queue.put((half, way))
    # sentinels
    for unused in xrange(cpus):
        in_queue.put((None, None))
    procs = []
    for unused in xrange(cpus):
        proc = multiprocessing.Process(target=count_combos,
                                       args=(in_queue,
                                             out_queue,
                                             compatible_ways))
        proc.daemon = True
        proc.start()
        procs.append(proc)

    half_memo = {}

    num_ways = len(ways) - len(mirrors)
    for ii in xrange(num_ways):
        (unused, prev_way, count) = out_queue.get()
        half_memo[prev_way] = count
        if prev_way in mirrored:
            half_memo["".join(reversed(prev_way))] = count
        print "(%d/%d) %s mirrored=%d count=%d" % (
            ii + 1, num_ways, prev_way, prev_way in mirrored, count)

    for proc in procs:
        proc.join()

    rest = (height - 1) - half

    for way in ways:
        if way not in mirrors:
            in_queue.put((rest, way))
    # sentinels
    for unused in xrange(cpus):
        in_queue.put((None, None))
    procs = []
    for unused in xrange(cpus):
        proc = multiprocessing.Process(target=count_combos_memo, args=(
                                       in_queue, out_queue, compatible_ways,
                                       half_memo))
        proc.daemon = True
        proc.start()
        procs.append(proc)

    total = 0
    for ii in xrange(num_ways):
        (unused, prev_way, count) = out_queue.get()
        if prev_way in mirrored:
            count *= 2
        total += count
        print "(%d/%d) %s mirrored=%d count=%d total=%d" % (
            ii + 1, num_ways, prev_way, prev_way in mirrored, count, total)
    for proc in procs:
        proc.join()
    return total


def main():
    try:
        width = int(sys.argv[1])
    except IndexError:
        width = 32
    try:
        height = int(sys.argv[2])
    except IndexError:
        height = 10
    print W(width, height)


if __name__ == "__main__":
    main()

and here's the threaded version:

#!/usr/bin/env python

"""Project Euler, problem 215

Consider the problem of building a wall out of 2x1 and 3x1 bricks
(horizontal vertical dimensions) such that, for extra strength, the gaps
between horizontally adjacent bricks never line up in consecutive layers, i.e.
never form a "running crack".

For example, the following 9x3 wall is not acceptable due to the running
crack shown in red:

3222
2232
333

There are eight ways of forming a crack-free 9x3 wall, written W(9,3) = 8.

Calculate W(32,10).
"""

import sys
import multiprocessing
import threading
import Queue


sys.setrecursionlimit(100)


def gen_ways(width, blocks):
    """Generate all possible permutations of items in blocks that add up
    to width, as strings."""
    if width == 0:
        yield ""
    else:
        for block in blocks:
            if block <= width:
                for way in gen_ways(width - block, blocks):
                    yield str(block) + way


def build_mirrors(ways):
    mirrored = set()
    mirrors = set()
    for way in ways:
        rev = "".join(reversed(way))
        if way != rev:
            low, high = sorted([way, rev])
            mirrored.add(low)
            mirrors.add(high)
    return mirrored, mirrors


def find_cracks(way):
    """Return the set of indexes where cracks occur in way"""
    result = set()
    total = 0
    for ch in way[:-1]:
        total += int(ch)
        result.add(total)
    return result


def crack_free(tup1, tup2, cracks):
    """Return True iff tup1 and tup2 can be adjacent without making a
    crack."""
    return not cracks[tup1].intersection(cracks[tup2])


def find_compatible_ways(way, ways, cracks):
    """Return a list of crack-free adjacent ways for way"""
    result = []
    for way2 in ways:
        if crack_free(way, way2, cracks):
            result.append(way2)
    return result


def build_compatible_ways(ways):
    cracks = {}
    for way in ways:
        cracks[way] = find_cracks(way)
    print "done generating %d cracks" % len(cracks)
    compatible_ways = {}
    compatible_ways[()] = ways
    for way in ways:
        compatible_ways[way] = find_compatible_ways(way, ways, cracks)
    return compatible_ways


def gen_combos(height, prev_way, compatible_ways):
    """Generate all ways to make a crack-free wall of size (width, height),
    as height-lists of width-strings."""
    if height == 0:
        return
    elif height == 1:
        for way in compatible_ways[prev_way]:
            yield [way]
    else:
        for way in compatible_ways[prev_way]:
            for combo in gen_combos(height - 1, way, compatible_ways):
                yield [way] + combo


def count_combos(in_queue, out_queue, compatible_ways):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, count the number of results, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            count += 1
        out_queue.put((height, prev_way, count))


def count_combos_memo(in_queue, out_queue, compatible_ways, memo):
    """Read tuples of (height, prev_way) from in_queue, call gen_combos
    on each, chain the result of memo to the last result in the combo
    to get the total count, and put the count on out_queue."""
    while True:
        (height, prev_way) = in_queue.get()
        if height is None:
            return
        count = 0
        for combo in gen_combos(height, prev_way, compatible_ways):
            last = combo[-1]
            count += memo[last]
        out_queue.put((height, prev_way, count))


def W(width, height, cpus=None):
    """Return the number of ways to make a crack-free wall of size
    (width, height)."""
    if cpus is None:
        cpus = multiprocessing.cpu_count()
    ways = sorted(gen_ways(width, [2, 3]))
    print "done generating %d ways" % len(ways)

    mirrored, mirrors = build_mirrors(ways)
    print "have %d mirror images " % (len(mirrored))

    compatible_ways = build_compatible_ways(ways)
    print "done generating %d compatible_ways" % sum(map(
        len, compatible_ways.itervalues()))

    in_queue = Queue.Queue()
    out_queue = Queue.Queue()

    half = (height - 1) // 2
    for way in ways:
        if way not in mirrors:
            in_queue.put((half, way))
    # sentinels
    for unused in xrange(cpus):
        in_queue.put((None, None))
    procs = []
    for unused in xrange(cpus):
        proc = threading.Thread(target=count_combos, args=(in_queue,
                                out_queue, compatible_ways))
        proc.daemon = True
        proc.start()
        procs.append(proc)

    half_memo = {}

    num_ways = len(ways) - len(mirrors)
    for ii in xrange(num_ways):
        (unused, prev_way, count) = out_queue.get()
        half_memo[prev_way] = count
        if prev_way in mirrored:
            half_memo["".join(reversed(prev_way))] = count
        print "(%d/%d) %s mirrored=%d count=%d" % (
            ii + 1, num_ways, prev_way, prev_way in mirrored, count)

    for proc in procs:
        proc.join()

    rest = (height - 1) - half

    for way in ways:
        if way not in mirrors:
            in_queue.put((rest, way))
    # sentinels
    for unused in xrange(cpus):
        in_queue.put((None, None))
    procs = []
    for unused in xrange(cpus):
        proc = threading.Thread(target=count_combos_memo, args=(
                                in_queue, out_queue, compatible_ways,
                                half_memo))
        proc.daemon = True
        proc.start()
        procs.append(proc)

    total = 0
    for ii in xrange(num_ways):
        (unused, prev_way, count) = out_queue.get()
        if prev_way in mirrored:
            count *= 2
        total += count
        print "(%d/%d) %s mirrored=%d count=%d total=%d" % (
            ii + 1, num_ways, prev_way, prev_way in mirrored, count, total)
    for proc in procs:
        proc.join()
    return total


def main():
    try:
        width = int(sys.argv[1])
    except IndexError:
        width = 32
    try:
        height = int(sys.argv[2])
    except IndexError:
        height = 10
    try:
        cpus = int(sys.argv[3])
    except IndexError:
        cpus = multiprocessing.cpu_count()
    print W(width, height, cpus)


if __name__ == "__main__":
    main()

Anyway, on my old Phenom II 3.0 GHz quad-core CPU, under Kubuntu Linux 14.04, performance looks like this:

Python Elapsed time (mm:ss)
CPython 2.7.6 21:13
PyPy 2.3.1 7:06
pypy-stm 2.3r2 11:27

So, basically, PyPy is almost 3 times as fast as CPython, and pypy-stm adds enough overhead to be about 60% slower than PyPy. (Note that since this version already uses multiprocessing to run on multiple CPUs, STM is pretty much all overhead no benefit here.)

The more interesting result is for the threaded version. We'd expect it to be about 4 times as slow as the multiprocessing version on CPython and vanilla PyPy, and we'd hope it to be less than 4 times as slow on pypy-stm, showing a benefit from transactional memory letting single-process multi-threaded code avoid the GIL.

Unfortunately, I guess this program uses a bit too much memory for the current version of pypy-stm, as it consistently segfaults about 3 minutes in. Armin's blog post (above) warned that this might happen due to a bug in LLVM. Oh well.

Fortunately, my program takes command-line options that can be used to vary the size of the problem. So, instead of building a wall that's 32 units wide and 10 units high, let's build one that's only 18 units wide and 8 units high. (I picked those values experimentally, trying to find the biggest numbers that let the program complete successfully many times in a row on pypy-stm.)

First, the multiprocessing version:

Python Elapsed time (seconds)
CPython 2.7.6 0.052
PyPy 2.3.1 0.167
pypy-stm 2.3r2 3.28

So, due to JIT startup overhead, CPython is actually faster than PyPy here. pypy-stm's overhead looks really bad on the smaller problem size.

Then, the more interesting result, the threaded version:

Python Elapsed time (seconds)
CPython 2.7.6 0.044
PyPy 2.3.1 0.176
pypy-stm 2.3r2 1.21

Threads are actually slightly faster than multiprocessing on both CPython and vanilla PyPy here, I guess because the overhead of forking subprocesses exceeded the benefit of using multiple CPUs on such a small problem size. Note that pypy-stm is significantly faster on the threaded version than the multiprocessing version, at least on its best run. (All results shown are best of 3 runs.)

One interesting thing is that pypy-stm's performance was highly variable. Over the 3 runs, I saw speeds varying from 1.21s to over 3s. It appears there's an element of luck in whether the transactions collide, and when they do, the code takes longer to finish (but still gives the correct result.)

In conclusion, pypy-stm is still highly experimental, will crash if you give it a program that uses too much memory, and isn't fast enough yet to be helpful in this benchmark. Even though Python programmers love to whine about the GIL, the multiprocessing module already gives a pretty nice way around it, so the bar for pypy-stm to be practical in production (as opposed to a nice theoretical result) is pretty high. Still, seeing transactional memory work at all to parallelize threads in Python is very impressive. (Haskell has had a nice STM implementation for a while, but then Haskell doesn't allow side effects in most functions so it's a lot easier to parallelize than a language like Python.)

I'll be donating to the next phase of the pypy-stm effort, and looking forward to better results in the future.

Python

Comments (0)

Permalink

DC Randonneurs Pastries and Coffee 209 km Brevet

DCR doesn't usually run brevets in July or August.  It's often really hot and lots of people are on vacation. But this year, at the last minute, we added a new 200 out of Severna Park, Maryland.  I hadn't ridden much in the Baltimore 'burbs, so this was a nice chance to see some different roads.

The route, shown here, starts in Severna Park and basically just scribbles north and south all over the place. You wouldn't think that it would be very hilly, and you'd be correct if you're comparing to a route that crosses actual mountains, but there are surprising number of streams and rivers to cross in that part of Maryland, and every one is preceded by a downhill and followed by a little climb. Somewhere around 6000 feet of climbing in 130 miles.

The weather was forecast to be unseasonably cool, which meant starting in the low 70s and peaking in the high 70s, which meant no concerns about clothes. Shorts and short-sleeve jersey all day. I did pack a cycling cap in case it rained, but didn't bother with a rain jacket since at that temperature it would just mean getting wet from sweat instead. I made sure to apply sunscreen, and brought some Ibuprofin in case my knee acted up, but otherwise it was a really easy packing job.

I resumed bike commuting (25 miles/day) on July 2 (after about two years of telecommuting a.k.a. not riding enough), so I was curious whether 2.5 weeks of steady riding would help with my 200 km pace, and with the stability of my left knee. I figured it was probably not enough time to make much difference.

It's about an 80-minute drive from my house to Severna Park, so I got up at 4:45 and left the house around 5:15. Arrived at Big Bean around 6:30 and the place was overrun with cyclists. There had only been about 20 riders pre-registered when I signed up a week before, but about 50 showed up to do the ride. They ran out of brevet cards and had to improvise for the last couple of riders. So I guess that shows that people will show up for a July brevet, if the weather is nice. I was happy to see Chuck and Crista there on their tandem, for the first brevet in a while. And Chris on a flat-bar hybrid, since he broke his trusty Litespeed (which looks a lot like mine except shinier) on the 600 and hadn't got another road bike yet. (I think it was the first flat-bar bike I've ever seen on a brevet, not counting recumbents.)

The big group headed out of Big Bean at 7, with ride organizer Gardner leading. I started near the front, around fifth, and stayed there for the first couple of miles as we went around 20 mph on nice suburban roads. Then we hit the first real hill and I instantly dropped back about 20 places. I wasn't going to overdo it early (this time), so I just shifted down and climbed sedately.

The hills broke the big pack up into several smaller groups. I ended up in a big group, which was moving a bit too slowly for my taste, so I sped up and chased down a group of five riders, Carol and several guys I didn't know. Carol's usually pretty fast so I figured if I was with her I was doing okay. We went up and down a bunch of rollers, got sprinkled on a bit but not enough to soak our feet, and soon enough reached the 25-mile info control, which involved writing down the number of the Knights of Columbus chapter that had adopted that highway.

We jumped back on our bikes and resumed churning. I eventually decided that group was a bit too fast for me and dropped off the back, so I rode alone for most of the next 17 miles. Somewhere in there I decided I might be a bit low on calories and ate a Clif Bar. The first real control was at Honey's Harvest in Rose Haven, right next to a marina on the Chesapeake Bay. There were a lot of bike people there, from our ride plus others. Honey's Harvest appeared to have real food, but I didn't want to wait for it and decided to just get ice cream and Gatorade to save time.

Leaving the control, I reversed course and was quickly passed by another rider with the same DCR jersey as me. Then by Chuck and Crista on their tandem, who are still faster than me despite their time off. We moved back inland a bit, until the second info control at mile 55. When I got there, there was a rider stopped at the corner, looking for the sign and not seeing it. I found it — it was an ad for a stable and we had to write down one of the services provided there. By the time I was done scribbling there were 3 or 4 more riders there, but I left first and none of them matched my speed.

We rode parallel to the Patuxent River for a while, then crossed Route 50, and then I caught up with Chris (on his hybrid) and rode with him for a while. He was climbing a bit slower than usual due to the extra weight of the bike, but that was fine with me. Several other riders passed us, then we passed them back, then they passed us again, and at some point I dropped off the back and was riding alone again. My left knee had started aching a bit around mile 35, and was we approached mile 70 it was getting bad enough that I decided to take Ibuprofin at the next stop. So, not surprisingly, 2.5 weeks of commuting was not enough to totally fix my knee.

I caught up with Chris again, who was riding with Leslie, who I hadn't seen earlier in the ride because she wasn't actually doing the brevet, just riding some of the same roads. There's a rule that you're not allowed to draft off people who aren't doing the same ride (to prevent people from hiring ringers to pace them for short sections of PBP), so I scrupulously avoided doing that. We had another info control at another stable sign at mile 71, where Chris and I stopped and Leslie kept going. I took my Ibuprofin, which worked, and my knee didn't bother me again for the rest of the ride.

Around mile 78, we got caught by a group including (the other) ride organizer Theresa, just before we had to cross Route 3 and optionally stop at a 7-11. That looked like a complicated crossing on the cue sheet (it was recommended to take a pedestrian crossing on the left side of the road), so I decided to follow the herd. About 6 of us waited approximately forever (probably actually 3 minutes) for the light to change, then crossed Route 3 and went to 7-11, where I bought 64 oz. of Gatorade and more ice cream. I refilled my bottles, chugged the rest of the Gatorade, enjoyed my ice cream, and sped off alone to steal a few minutes from those who were faster riders but longer stoppers.

A few minutes later, the sugar rush from the ice cream hit, and I got my second wind and rode fast for a bit. I caught and passed Carol, who was suffering from leg cramps. But then I needed to stop in the woods to water some trees, and a whole group of riders passed me. (Actually getting far enough from the road to avoid getting charged with indecent exposure costs a lot of time.) The sugar rush wore off and I was back to riding at 15 mph, as the cue sheet dumped us onto the BWI airport trail. It's pretty much just a big sidewalk next to the airport fence, not much of a trail. While I was slowing down to make sure of a turn, George caught me, and I rode with him for a while, trusting that he knew where he was going since he lives in Maryland. But, just like on the 300 and the fleche, George was a bit too fast for me, and I eventually got dropped.

Right around the 100 mile point we turned onto Ilchester Road, but fortunately it was above the infamously hilly part. The outer edge of my left foot was bothering me, a problem I'd never had before. I decided to sit down in a shady spot for a bit, take off my shoes, and massage my feet. That seemed to help, though it cost me a few minutes, and Joel and Calista (who had been tricked into stoking Joel's tandem) passed me while I was sitting there. I decided to get back on my bike and caught up with them and followed them for the next few miles into downtown Ellicott City. We had an open control there, and I figured one of them would know a good place to stop. We ended up going to a yuppified coffee shop, where I had a gigantic muffin, then left while they were still eating. (It was apparently my day for fast stops. Or I was just worried about having my bike stolen since there were a ton of people there.)

The next section involved the Grist Mill Trail in Patapsco Valley State Park, which was really nice. There was a skinny little pedestrian bridge to cross to get on the trail, then 2.5 miles of beautiful, shady, paved trail right next to the river. Lots of pedestrians and joggers and dog-walkers and little kids to avoid, but I wasn't in a hurry and they were all courteous. Then the route proceded down a "decrepit" road (first time I'd ever seen that word on a cue sheet) leaving the park, an old park entrance road that's no longer maintained and now closed to cars by a gate. No signs prohibited bikes from using it, though. It was a very rough road but I didn't get a flat so no problem.

After leaving the park, I crossed the Patapsco River, then caught up with Chris and Carol as they approached a traffic circle. They were moving at a nice sedate pace so I stuck with them for the rest of the ride. We had to navigate two traffic circles (got one right, got one wrong but figured it out quickly enough) and some more BWI Airport Trail, a few more small hills, and finally a couple of miles on the B&A Trail at the end of the ride coming into Severna Park. The end control was at Squisito's, which has pretty good pizza. We finished in 9:34, not fast for a flattish 200, but not that bad either. (Way better than the 10:20 I did on the Wilderness Campaign 200 in March.)

Overall, it was a good ride. Big turnout, perfect weather, no big problems with drivers or dogs. Though we later heard that one guy crashed and trashed his back wheel, but he wasn't injured. I would ride this route again.

Bicycles

Comments (0)

Permalink

Team Double the Blues Fleche

Looking at the DC Randonneurs calendar for 2014, I decided not to ride the fleche.  First, I wasn't sure if I could handle the 360 km (223 mile) minimum distance, since I hadn't finished a ride that long since fall 2012.  And I particularly didn't want to fail to finish a team event, where I'd be hurting my teammates not just myself.  Second, it was scheduled for the week before the 400 km brevet, so even if I could finish, I might not be able to recover, and might end up having to miss the 400.

After the 300 km brevet, two weeks before fleche, George W. asked if I might want a spot on his fleche team.  I was sore and tired after a poor finish to that ride, so I told him I was not interested.  Then, a few days later, Nick asked me the same question via email.  By then I was no longer sore and the pain of the last 20 miles of the 300 had started to recede from my memory, so I changed my mind and decided to ride. (Without such selective memory, there would be no endurance sports. We'd all be smart enough to stick to non-painful distances.)

Just a few days before the fleche, Christian crashed his bike on gravel and had to withdraw from the team, leaving us with only 4 riders.  We were prepared to ride with 4, but then Dave S., who had previously dropped out of the team, decided to rejoin, so we were back to the full 5.

We had some a few small changes to last year's route.  An optional gravel detour (to avoid a few miles on high-traffic Route 522) was added to the route, but everyone except Nick decided not to take it.  The Westover 7-11 closed, so we changed the start control to the Wells Fargo ATM next door.  Tolliver's Grocery (with barbeque) and Baker's Store weren't really needed to guarantee the 360 km distance, so they were removed as controls.  The updated route is here.

The pre-ride forecast was for about 50 degrees and dry at the start, wind and a 25% chance of rain during the day Saturday, a high around 75, and then a low in the low 40s Saturday night. So we had to be prepared for cold, wet, and sun. So I packed bike shorts, a poly jersey, sunscreen, a rain jacket, tights, arm warmers, a wool jersey, a balaclava, a reflective vest, fingerless gloves, full gloves, lobster claws, cotton socks, wool socks, and shoe covers. So I was prepared for any conceivable weather, at the cost of having my bags stuffed with clothes.

Nick invited us to crash in his basement, since his house is right near the start of the route. George and Mike and I accepted his offer, while Dave decided to sleep at home and meet us before the ride. I ate dinner (burritos), packed my bike and left my house around 5:30 Friday night. Unfortunately it was raining hard, which probably made rush hour traffic worse, so it took me about 90 minutes to go 26 miles to Nick's house. Hindsight says Dave made the right call, since there's no traffic at 3 a.m.

We went to bed by 8:30, but I couldn't fall asleep until about 11. And the alarm went off at 3:30. So a solid 4.5 hours of sleep. We were all ready to go by 4:30, and rolled downhill to the start a few minutes before 5. I paid the $3 foreign ATM usage fee to take out $20 that I didn't actually need, just to generate an ATM receipt that proved I was there just before 5:00. It was around 50 degrees, and I was kind of chilly in my wool jersey, arm warmers, light tights, and light gloves. But I figured I'd get hot if I put on my jacket, so I left it in the bag and dealt with the chill.

The route did about a mile on mostly-empty Arlington streets, and then joined the mostly-empty W&OD Trail for the next 38 miles. We saw a bunch of rabbits, a pair of deer, and a couple of joggers in the pre-dawn dark. Then the sun came up and we saw a few early-morning riders and more joggers. Around the Loudoun County line, the group split up, with George and me out front. But we kept the pace reasonable and the others eventually caught up. It turned out there was a 5k run on the trail, but we didn't see any big crowds, so either it was a small run or we got through before it started. I needed to use the porta-potty in Hamilton, so I charged off the front intending to finish before anyone else caught up and avoid slowing the group. But this plan failed when a couple of other riders decided they also wanted to use the facility.

We reached the McDonalds in Purcellville right around 8, on schedule. The people working there had a hard time with the concept of initialing our brevet cards, but we eventually got one to do it. This McDonalds had calorie counts for each item right on the menu, probably not to help endurance riders get as many calories as possible to avoid bonking. I had a sausage and egg biscuit with hash browns and orange juice. It was okay. Our schedule said we had 18 minutes at McDonalds, which I thought was optimistic, and indeed it took us more like 20. Close enough.

The next part of the route was really nice, but kind of hilly. We went south from Purcellville, down Snickersville Turnpike and Sam Fred Road to Middleburg. At this point I was feeling great. George and I were taking turns at the front, with periodic stops to regroup. I didn't know it at the time but Dave was having problems on some of the climbs. (He looked fine to me.) We went through Middleburg and turned onto Halfway Road toward The Plains. I didn't see the other 3 riders, but George thought they may have just turned one street early and cut over on the back road, which indeed they had. They caught up, and all rode as a group for a while, with a couple of stops to deal with minor equipment issues.

George and I went off the front again, and waited for the rest at the 55 intersection, as a huge group of motorcycles (I think a charity ride for veterans) went by. A few minutes later, when my hearing had recovered, we started rolling down 55 to the east. It wasn't very crowded (the motorcycles were going west) so it was a very pleasant ride up and down the big rollers. Nick and George and I were together in front, with the others not far behind. At mile 66 we turned off 55 onto Blantyre, and that's when I first started to get a bit tired, as George's pace was a bit fast for me and I watched him pull away into the distance. The turn onto uphill Blackwell Road surprised me, and I didn't shift down quickly enough, and got a horrible crunching noise from my drivetrain. Fortunately, it was only chain suck, and I'd stopped pedaling before the chain could do any damage to the front derailleur. So I unjammed it and continued.

At one point on Blackwell Road, I was zooming down a nice straight descent enjoying the breeze and not paying enough attention to the road surface. When suddenly a giant pothole appeared right in front of me. I had to very quickly choose between trying to swerve around it (and maybe falling), trying to bunny hop it (and probably failing), or just hitting it (and probably wrecking a tire and maybe a rim). I swerved, and somehow missed it. I paid more attention after that; the rough winter has given us a bumper crop of potholes.

I rolled into the Sheetz outside Warrenton around 11:15, a few minutes ahead of schedule. George was already there but hadn't gone inside yet. I stayed out to watch bikes while he went in, then when he came out I went in to get food, and while I was getting food everyone else arrived. So we were still on schedule. I had a meatball sub and an ice cream cookie sandwich, both of which were good.

We left the Sheetz as a group and climbed through hilly downtown Warrenton. That part features lots of turns so I stayed off the front to avoid leading others astray. In a couple of miles we got onto Springs Road heading out of town, which had some traffic and some hills but was nice enough. I was still feeling pretty good, but George was clearly faster. We mostly stayed together for the next several miles, until Rixeyville. There was too much traffic on Rixeyville for my liking, so I decided to take it fast to get off it sooner, and as a result pulled ahead. I stopped at Ma & Pa's market to wait for the others. George pulled in right behind, the others a couple minutes later, and we decided to not stop at Ma & Pa's and instead stop at Reuwer's Store in 10 more miles. Since it had been 20 miles since Sheetz, I ate a really nasty-tasting Roctane gel. (I bought a Gu variety box a while back, so it's like those Bernie Botts Every Flavour Beans from Harry Potter, a random mix of yummy and vile flavors. Next time I'll just get a box of all vanilla. Boring but safe.)

The next road was Monumental Mills, which was apparently paved with a cheese grater. It was super-buzzy and I felt most of my pedaling effort going to fight road friction. Definitely the kind of road where fat tires make you faster and 25mm tires make you sad. Luckily we were only on it for 4 miles, and my speed picked back up afterward. There were some more hills on Eggbornsville and then we were at the Reuwer's Store stop. George was ahead of me again, and I was a bit tired but still doing okay. I had a Vanilla Coke, and for the second ride in a row it didn't sit well in my stomach, so I'll be avoiding soda on my next ride.

Our group fragmented again on the next section. George was still very strong, and got far enough ahead of me that I couldn't see him except on long straight sections. So when I got to the Reva Road T intersection, and had to choose between left and right, and the cue sheet said "S", I was stuck. I decided to just wait there until someone else caught up, rather than guessing. Dave caught me a couple minutes later, and his GPS said to turn right, so I went with him. We rode together until mile 115, when I had to stop to flip my cue sheet and note the problem so Nick could fix it before next year. That let Dave get far enough ahead that I couldn't see him when I got confused again by a turn at mile 116. So I waited for Nick and Mike, who arrived a couple minutes later and reassured me that I was on course. I rode with them until we reached 29, a fast high-traffic road with a decent half-shoulder. We rode the shoulder for a while, then Nick said he knew how to exit the highway early and take a calmer parallel road to the control. So I followed him and it was indeed much nicer. And we reached the 124-mile Madison McDonald's control, where George and Dave were waiting. Mike was just a minute or two behind us. We were still on schedule.

At McDonalds, I consulted the convenient calorie counts on the menu sign and saw that the Double Quarter Pounder had the most of any burger on the menu, so I got that. With fries and a Coke. (I'd already forgotten that the earlier Vanilla Coke hadn't sat well in my stomach.) The burger and fries were excellent, with the salt on the fries tasting extra-good (so maybe I was low on salt?), but the Coke was flat. Not just one flavor: the whole fountain. George told them, but they didn't fix it, at least not in the 45 minutes we were there. [Note to fast food managers: if a huge percentage of your profit margin comes from marking up a few cents worth of branded fizzy sugar water, you had better make really sure the soda is consistently good. Being lazy about quality control here will cause your customers to switch from high-profit soda to free water.] Everyone seemed to be in pretty good spirits; there was a lot of picture-taking and Facebook posting.

Someone mentioned that, with the Baker's Store control removed from the route, the next water stop might not be for 45 miles. (There's a store on Ely's Ford around mile 158 but we weren't sure if it would still be open.) This concerned me a bit because I only had 66 ounces of water on my bike: 2 Zefal Magnum bottles and no Camelbak. But we were approaching dusk and it wasn't that hot (probably low 70s), so it might be enough. And if it wasn't, I could always go a bit off-route to Baker's Store.

We put on our reflective gear (it wasn't quite dark yet but would be pretty soon) and left Madison as a group. A few miles later we fragmented again; George was off the front, then Dave and I, then Nick and Mike. But Nick took the gravel shortcut detour while the rest of us took 522, so when we reached the turn off 522 onto Algonquin Trail, Nick was waiting for us along with George. We waited a minute for Mike to catch up, and watered some trees, and ate some snacks, and then did the really nice Algonquin – Batna – Lignum stretch. George and Nick were up front, with Mike and Dave and I behind but within sight. I was starting to tire a bit. Mike and I both wanted to stop for a bit to switch sunglasses for clear glasses, so I suggested the Lignum post office, and went forward to the leaders to convey the message. We stopped for a couple of minutes in Lignum right at dusk, then George thought we were taking too long and started riding again, which was very effective at getting the rest of us moving.

The plan was to stay together after dark, but we failed, and ended up splitting into two groups, with George and Nick and I in front and Dave and Mike behind. We crossed the Rappahannock at Kelly's Ford, having decided to stop at M&P Pizza at mile 170 instead of the Inn at Kelly's Ford at mile 163. (We were afraid there might be a long wait for food at the Inn.) There's a big hill on Sumerduck right after the river crossing, which my legs didn't like much. George was still really strong and accidentally dropped Nick and me, which cost him when he missed the turn onto Courtney's Corner. We were still within sight, and did a lot of yelling and whistling, but he didn't hear us, so we stopped and waited for him to realize his mistake. He did, and decided to stay behind us for a while. I didn't really feel like navigating in the dark so I stayed behind Nick, and the three of us continued at a sane pace for the next 5 miles to pizza. I was definitely tiring, at around the same distance where I got tired on the 300 two weeks earlier. (So I guess the 300 didn't really help.)

We got to M&P Pizza at mile 170 right on schedule, and Dave and Mike arrived a few minutes later. Dave got a flat tire right before he reached the restaurant, close enough to walk the bike there and fix it later. Mike wasn't going well; he had an upset stomach. I ordered spaghetti, which was pretty good, and a vanilla milkshake, which was okay. Mike didn't want to eat anything, but Nick made him get some spaghetti and he ate a tiny bit of it. He also took some antacid, which eventually helped some. But Mike didn't look good at all and there was some discussion of what we'd do if he couldn't finish. Dave and I hadn't finished a fleche before, and Nick needed to be at the finish on time to receive the other fleche teams, so Nick thought George should stay with Mike if needed. George wasn't so happy about that. Of course we all preferred for Mike to finish, but it didn't look likely. After an hour of eating and resting and tire-fixing, Mike was willing to give it a shot, and we resolved to all stay together to look after him. I developed a bad case of gas and decided to stay at the back as much as possible to avoid fumigating my teammates.

After M&P, we were on Elk Run for over 9 miles. There was some traffic so we mostly stayed single file, with Nick leading then George, Dave, Mike, and me bringing up the rear. After a very long pull, Nick asked to move back, so George took over at the front. We eventually pulled over for a brief rest on Aden Road, with a view of a field of McMansions in the middle of nowhere. We turned onto 234 (old-style small road portion), then onto 234 (might-as-well-be-an-interstate portion), and then onto Minnieville Road. I grew up in Dale City so Minnieville is familar to me, but there's been so much construction since that I only recognized the older bits mixed in between the new stuff. I pulled the group down Minnieville, but it was hilly and I pulled a bit too fast on a couple of uphills (I guess the spaghetti and milkshake kicked in) and had to be slowed down. Then, right as we approached the Dale Blvd. intersection, my chain got sucked into my front derailleur again. Second time on the ride, so I knew what it was instantly and fixed it (without lasting damage) quickly. It's a new chain, and I might need to shorten it a bit. Also, the switch in one of my taillights failed, and the light was periodically turning itself off. That's why you always ride with at least two. Of course, that one is getting smashed with a hammer and replaced before the 400.

We continued down Minnieville all the way to Old Bridge, then turned right toward Lake Ridge. There was some traffic but the roads are wide and have lots of shoulders and right turn lanes so we were able to mostly stay away from it. Old Bridge turns into windy twisty downhill sweepers as it approaches the Occoquan River, and I had a hard time keeping my hands off the brake levers so I could keep contact with the group. Unfortunately, what goes down must go back up, and after we crossed the bridge into Fairfax County we had to climb a long way up the debris-strewn shoulder of 123. Luckily nobody got a flat. After way too long on 123 we got to turn onto usually-low-traffic Lorton Road, but there were tons of cars coming the other way. (Apparently there was a big backup on I-95 that people were avoiding.) We had several more miles of rollers before reaching the 209-mile control at a Lorton 7-11 around 1:30 in the morning, a few minutes behind schedule.

It was only supposed to be a 5-minute stop but Mike was pretty beat and it took a few extra minutes to get him moving. He seemed fine on the bike but in bad shape off it. I think it was upset stomach leading to inability to eat or drink much leading to bonk and dehydration. We discussed whether to make our 22-hour control stop at a 7-11 in Springfield or at the Silver Diner in the ruins of Springfield Mall. (There's a big hole in the side of JC Penney, along with a sign that says it's still open during construction.) I thought we agreed on the 7-11, but then George lobbied for the Silver Diner because it was getting chilly and he thought sitting in a warm booth was much better than sitting on a cold stoop, even if we had to ride a few miles out of our way, and we decided he was right. I wasn't navigating at all anymore, just following wheels and hoping someone knew where we were going. There was some confusion in the mall parking lot but we eventually found the Silver Diner and pulled in around 2:10 in the morning.

The 5 of us walked in in our matching reflective vests, to the amusement of the local teenagers. I ordered a BLT, George got a milkshake, Dave got some soup, and Nick and Mike did some napping. We had to stay until 3 a.m. per fleche rules, designed to force you to take almost the full 24 hours rather than just zooming to the finish. (That rule wasn't really needed for our team, except George. We needed the time.) Eventually 3:00 approached and we had to pay our bill and get back on our bikes. It was only 17 miles to the finish and we had 2 hours to do it, but Mike was still not looking great, and I was pretty tired too. We had to go down this crazy slalom trail under the Beltway with turns that were sharp enough that I had to put a foot down. Then we got to drive through normally-busy Alexandria with almost no cars, which was surreal. We ran every red light after verifying there were no cars around; the only time there was a car coming, four of us yelled "car left" in quadrophonic stereo, as if we'd spotted something rare, wonderful, and dangerous.

Five miles from the end, we just had to get on the Mt. Vernon Trail. But — surprise — the bridge over the Four Mile Run was out, with a detour sign. So we tried to follow the detour to cross the stream on a road and rejoin the trail, but (as usual) the detour signs weren't as numerous or well-placed as they should have been, so we spent several minutes hunting for a way across. I was about to suggest that we give up on the trail and just take roads to the finish, when someone found the next detour sign and we made it across the stream and back onto the trail. The Mt. Vernon trail is a handful at night — it's narrow, twisty, bumpy, and poorly marked. (During the day it can also be crowded, but that wasn't a problem at 4 a.m.) Luckily it's on Nick's regular commute route, so he was able to lead us to the finish with no problems.

We rolled into the Key Bridge Marriott well ahead of our 5:00 deadline, and got to do our paperwork then wait for all the other teams to arrive. (We were first at 5, with most of the other teams finishing between 6 and 7. This was because we started earlier, not because we were faster.) We got a bit of sleep on the lobby's couches and chairs, then the hotel started waking up. There was a women's half marathon starting a couple of hours later, so we got to chat with a few runners. Then there was a steady stream of arriving fleche teams, and a buffet breakfast.

We all finished. Mike looked a lot better after he got off the bike, but he and Dave and I were all pretty worn out. George and Nick looked ready for some more riding. We ate our buffet breakfast pretty early, then went back to greet arriving riders. George rode to where his van was parked, then went home. Dave and I didn't want to ride 5 uphill miles to Nick's house where our cars were parked, so we hung out until all the festivities were done and squeezed into Nick and Jan's minivan. I was very tired, but had napped enough in the lobby that I felt safe to drive home, so I did that, then slept from 9 to 2.

Any fleche where the whole team finishes and nobody gets hurt is a success. I was happy with my riding in the first half but disappointed that I faded so badly at the end. Unfortunately, there's only one week until the 400, not long enough to do much about it. Dragging your fatigued body along at 10 mph for the last few miles is an important skill that I wish I didn't get to practice so often.

Bicycles

Comments (0)

Permalink

DC Randonneurs Warrenton 300 km Brevet

After the cold, wet, hilly Paul's Paradise 200k, I was ready for spring and fewer hills.  The Warrenton 300k is relatively flat, and April is warmer than March.  The forecast a few days out had a chance of rain on Saturday, but as the ride got closer the chance of rain went down to zero.  I packed my rain jacket anyway, but was cautiously optimistic.

This year's innovation was adding some gravel sections to the route.  I mostly ride brevets on a road bike with 25mm tires, so gravel doesn't thrill me.  The pre-ride report was that some of the gravel was new and not easy to ride on skinny tires.  I do have a touring bike with 30mm tires, and I would have ridden it if the gravel were mandatory, but there were paved options to bypass the gravel so I decided to skip it so I could ride the lighter bike with better brakes.

So far in April, I'd done a couple of 20-mile rides Wednesdays after work, and a 60-mile ride the previous Saturday.  Not a lot, but, on top of the 2 200-km brevets I did in March, I thought I was approaching a reasonable mileage base for a 300.  My main concern was my left knee, which got sore enough to make me bail out of the fleche last April at around 150 miles.  I hadn't done that distance since, so I couldn't be sure my knee would make it 300k.  But I thought the recent rides were helping it, since it started hurting around 50 miles in March but I made it 60 miles without pain in April.

My weight had been 203 lbs. (dehydrated and glycogen-depleted) on Thursday, but two days of pre-ride carbo loading had it up to 211 (glycogen-stuffed and retaining water).  A bit more than I wanted; next time I'll eat light on Thursday and only eat heavy on Friday.

Our 300s start at 5 a.m.  I live about 45 minutes from Warrenton, so I packed the bike the night before and set the alarm for 3.  I tried, but failed, to get to sleep early, and so only got about 4 hours of sleep.  I threw several caffeinated Gu packets in my bag, just in case I got sleepy during the ride.  I also brought a couple of Clif Bars for calories, my 1L Zefal Magnum bottles since the forecast high was in the 70s and there were some pretty big gaps between stops, and started with Gatorade in the bottles for even more calories.  And had a bowl of cereal for breakfast.

With forecast temperatures ranging from the high 40s to the low 70s, I wore summer shorts and a summer synthetic jersey, arm warmers, light tights, reflective vest, light full gloves, cotton and wool socks, and summer mountain bike shoes.  I packed but did not wear a light balaclava and a rain jacket.  I remembered to apply Lantiseptic but forgot to apply or bring sunscreen.

We got a good turnout for a 300, 52 people.  I arrived early enough that there was no rush for bike inspection or registration.  There was a nice spread of pre-ride carbs, so I had two mini-scones and a homemade cookie.  Operation Do Not Bonk was right on schedule.  I was a bit chilly standing around outside, which is about right to avoid overheating once the pedaling starts.

After the pre-ride speech, we rolled off and I ended up at the front.  The light to turn onto 29 was red, and I didn't remember if the sensor could detect bikes, so I rode over and hit the pedestrian button.  Unfortunately, it only controlled the crosswalk, not the light, so we all ended up running the red when it was safe.  (The best thing about 5 a.m. is that there's no traffic.)  I remained in the lead for the first couple of turns, but eventually someone else blew past me and took over the navigational chores.  The lead rider got confused and tried to turn right a bit early, which caused me to stop to avoid hitting him, and a paceline of about 20 riders blew by on my left.  I tucked into the back of that group, then gradually slipped back over the next 5 miles or so as we went north toward 55 in the dark.  There's a tradeoff between saving energy by drafting and saving energy by not going too fast, and I'm never sure I have it right, but I ended up at the back of the second or third group, which was going fast enough that I didn't feel too lazy and slow enough that I didn't feel too stupid.

I knew there was probably going to be a secret control somewhere on 55, because I manned it last year.  Paul, riding just ahead of me, suddenly sprinted off the front of the group, and I wondered if he knew where it was and was trying to reach it first.  I decided to save energy and not chase him.  Sure enough, the control was there a couple miles later.  And because I was at the back of a big group, I had to wait a couple of minutes to get my card signed.  No big deal.  The control did split up the pack, as riders trickled out alone or in pairs.  So much for the draft.

I rode alone, but within sight of several riders ahead and behind, down 55 to Marshall.  The sun was coming up as we turned south on Free State.  Just over the the bridge over I-66, I was looking for the turn onto Crest Hill, and wasn't sure if I was at the right place because I didn't see a sign and I didn't trust my odometer calibration.  Then two riders behind me yelled and took that turn, so I figured they must know and followed them, and fortunately they were correct.  We had almost 17 miles on Crest Hill, which was really nice.  Still not much traffic, and hilly but not steep.  A group of 5 riders passed me during that stretch, and I rode close to them for a while but eventually let them go, since I wanted to keep my speed down to conserve energy.

We zipped through the village of Flint Hill and then onto Fodderstack, which is hilly and pretty just like Crest Hill.  This went through Little Washington and past the famous Inn.  Unfortunately, the bucolic back roads had to eventually end, and we got dumped onto Route 522.  Only for a mile, though, and it was still pretty early so the traffic wasn't too bad yet.  There was a Shell station, but I decided not to stop since I was making good time and still had plenty of Gatorade left.  That reminded me that I hadn't eaten anything since the start of the ride, so when after we turned onto Rudasill Mill I stopped for a minute to eat a Clif Bar and water some trees and and turn off my taillights and stow my arm warmers (which had been rolled down into the wrist warmer position for a while).  During that brief stop, several riders passed me.

After a few more miles on back roads, we got dumped on 522 again for a bit, then turned onto F.T. Valley (not to be confused with Ft. Valley, which is two valleys to the west) for 10 miles.  This was one of the roads with a gravel bypass that I didn't take, and the traffic wasn't that bad and (I heard later) the gravel was new and hard to ride, so it was a good call.  Still, 10 miles with fast cars isn't so fun, and I was happy to finally turn off onto Etlan Road toward Old Rag.  We got a great morning view of the mountain, and then the big nasty rough sweeping downhill toward Syria.  I'd climbed this hill at least 5 times on the Old Rag 200, but had never gone down it before, and was a bit worried.  It turned out to be not too bad, though I took it a lot slower than the rider ahead of me who shot off into the distance.  And another pack of about 5 riders passed me right before we reached the 65 mile Syria Mercantile control.

I used the bathroom, bought sunscreen and Gatorade and a cookies-and-cream ice cream cup, and had a brief discussion with a couple of riders about whether it was warm enough for shorts yet.  My vote was yes, and I stripped down to summer cycling attire and lathered up with sunblock.  It was the right call, as it kept getting warmer after that and the daily high reached about 82.  I got back on the bike pretty quickly and followed a group of 3 riders through the familiar and very nice Hoover / Hebron Valley section.  My knee started to ache a bit around mile 75, but I didn't want to root around in my bag while moving, or make an extra stop, so I decided to wait until the cue sheet flip at mile 80 to take my Ibuprofin.  When I did, I saw that there was an info control in 4 miles.

The next 15 miles was a nice section with rollers and not too much traffic.  My knee stopped hurting about 8 miles after I took the Ibuprofin, which was nice, and I brought my speed back up a bit and passed George.  (We leapfrogged each other all day.)  I hit the 95-mile halfway point at 11 a.m., so 7 hours, or a 14-hour pace if I didn't slow down (which I figured I probably would).

The cue sheet said there was a Subway and Hardees in Gordonsville at 101 miles.  I thought about it for a few miles and eventually decided I wanted Subway.  But then the Hardees was right there on the route and I couldn't see the Subway, so I decided Hardees would do.  I went in with Chris, and George joined us a minute later.  I had a 6-dollar Thickburger with fries and a Coke Zero.  (I normally get full-sugar soda on long rides, but I forgot.)  Also refilled my bottles with water at their fountain, though they weren't quite empty so I ended up with very diluted Gatorade.  We chatted with a guy who was interested in our bikes, how far we were riding, etc.  Then Chris and I left together.  We were both worried about going the wrong way out of Gordonsville, but we got it right (though I almost missed the turn onto Kloeckner, seeing the sign at the last second).

I couple of miles later, I was riding well behind but within sight of Chris when I saw a gigantic gray dog (Mastiff-Great Dane-Elephant mix?) come out of its yard after him.  Luckily Chris had a head start and the dog stopped chasing him pretty quickly.  Unluckily it was already in the road when I got there.  Gulp.  Luckily it just wanted to say hi, not eat me.  Because I think it was bigger than me.  Immediately afterward, two tiny little yip-yip dogs went charging after Chris, but they were too small to be scary.  I yelled so he would see them and avoid running them over, which he did.  They were also waiting in the road for me, so I zigged left and zagged right and went around with no problems.

I approached the 107-mile info control riding near Chris and George and two other guys.  Chris and George and I stopped but the others blew past.  We yelled but they didn't stop, so we hoped they saw the sign.  (When I caught up with them later at a control, they said they did, so no problem.)  Lunch kicked in and I felt good for a while, which let me pull away from Chris and repass George.  We had another control at 120 miles, and I got a Klondike bar, and more Gatorade.  Roger was also there (he's usually faster than me so I'm always happy to see him in the second half of a ride) and I think he also got ice cream.

I was still feeling good when I noticed that the street sign ahead said Vawter Corner but the cue sheet said Vawler Corner.  Distracted by the typo, I turned right instead of left, and led a following rider (who I didn't even know was there) astray.  Luckily I caught the mistake right away and said "sorry, left turn" and got us back on course.  But I worried that my brain was starting to go.  My knee was hurting again, and it had been 40 miles since my previous Ibuprofin, so I took some more (and chugged a bunch of Gatorade to make really sure I was well-hydrated).  I gradually slowed down over the next 12 miles to Orange.  Orange was an open control, and I felt paralyzed by choice, but eventually decided on the 7-11 rather than a fast food place since I'd just had lunch 30 miles ago.  But, still obsessed with not bonking, I had a Mrs. Fields Klondike ice cream sandwich (way too much cross branding there, but it was good), my third ice cream of the day.  I remembered to get a receipt, and George pulled in while I was refilling my bottles.  We discussed the upcoming gravel section — I decided to stick to pavement and George decided to take the gravel.

That next section was up and over Clark's Mountain, which isn't much of a mountain but isn't flat either.  Then 4 miles on 522, which was awful.  Tiny shoulders and too much fast traffic.  One pickup truck passed me very dangerously, almost colliding head-on with a car coming the other way.  I decided to ride as fast as possible to get off 522 before someone hit me, and got my speed up over 20 on the flats.  But then I saw Bakers Store at mile 147, and figured I could use more Gatorade and a brief rest after the fast riding.  I also got a Good Humor Strawberry Shortcake bar (ice cream #4 of the day).  Whatever problems I would have this day, I wouldn't bonk.

Rested up, I waited for a big gap in traffic then sprinted the last mile of 522 (most of it downhill to the Rapidan) and was happy to turn off onto Algonquin Trail.  The next 10 miles on low-traffic back roads were very nice, though I was tired after sprinting.  I caught up to George again right before crossing Route 3, but then needed to stop and take my third Ibuprofin of the day at mile 160.  The familiar roads around Kellys Ford were nice as usual, except for the usual rough patches.  We crossed the Rappahannock at the usual bridge, and then turned left toward Remington on Summerduck.

The road into Remington was kind of low, with standing water in the fields to our right, perfect breeding grounds for bugs.   So I rode through my first disgusting gnat cloud of the year.  Followed quickly by several more.  Luckily I had sunglasses on and my mouth was closed, so I only got gnats all over my arms and legs.  Yuck.  I stopped one last time at the Citgo in Remington at mile 170.  I didn't think I could handle any more ice cream, and I was getting sick of Gatorade, so I got a Vanilla Coke instead.  Mistake.  I guess the carbonation riled up the giant mass of undigested calories that was already there, and my stomach was sour for the rest of the ride.

I wasn't sure if I'd finish before dark, but I figured it would be close, so I turned on my taillights and put on my reflective gear to avoid needing to stop later.  It was still hot.  I left the Citgo, carefully keeping it on my right side to make sure I was going the right way.  But then Route 15 wasn't there, and I realized I'd gone the wrong way.  The Citgo was on a corner so there were two ways to leave and keep it on my right.  Aargh.  Second brevet in a row that I left a stop in the wrong direction.  It cost me 1.5 bonus miles, and some morale.  But, whatever, still only 20 miles to the finish.  My legs were pretty shot and my stomach was still sour, so I rode the last 20 miles very slowly, but didn't make any more wrong turns.

I crossed 15, then a mile later crossed 17, then after a few miles crossed Meetze.  Calista passed me just a few miles from the end, and I tried to keep her in sight as a way of keeping my speed up, but then a car pulled out of a driveway right in front of me on Frytown, and I had to stop to avoid getting run over.  By the time I got going again she was gone and I was back to 10 mph.  The downhill on Duhollow was very fun (I think I descend better when I'm tired because I forget to be scared) and I saw her again, but then lost contact on the uphill part of Walker.  Finally, I turned one driveway too early, into whatever business is right before the Hampton Inn, and their parking lots didn't connect so I had to drag my bike over a couple of curbs and a few feet of grass to make the finish.

I finished at 7:59, for an elapsed time of 14:59.  I actually felt pretty good about my pace for the first 170 miles, then fell apart in the last 20.  No big deal in the context of a 300, but there's a 400 coming in three weeks, and I hope 20 miles of very slow riding doesn't become 80 in the dark.

Other than one insane truck driver, and the slow drag into the finish, it was a fantastic day.  Nice weather makes everything better.

Lessons learned:

  • Ice cream is good, but I can only handle so much of it while pedalling.  Limit to 1 per 100k in the future.
  • I need to carefully get my bearings upon arrival at a control, so I leave in the correct direction.
  • Ibuprofin works better than I remembered.  (I hope that means my knee is better than it was.)

Bicycles

Comments (0)

Permalink

DC Randonneurs Paul's Paradise 200 km Brevet

Our second 200 of the year was Paul's Paradise, a very hilly route.  The weather forecast was 40s and rainy.  Bleh.  If I were in better shape, I may have decided to skip it.  But with only 2 weeks until the 300, I really needed the miles, so I gathered up my rain gear.

I have two bikes with fenders.  One is a fixed gear — nope.  The other has fiddly cantilevers that are always going out of adjustment.  (I own some nicer Paul cantis but haven't got around to installing them yet.)  The thought of going down a wet Mar-Lu Ridge with brakes I don't really trust — nope.  So I decided to ride my Litespeed road bike, sans fenders.  I figured I'd eventually get soaked with or without them, and I probably wouldn't be going fast enough for anyone to want to draft me anyway.  Though, with hindsight, maybe I should have attached my Race Blades, which aren't as good as real fenders but are better than nothing.

I wore a short-sleeve wool jersey, shorts, heavy tights, cotton socks, wool socks, summer mountain bike shoes, a balaclava, light full-finger gloves, a rain jacket, and a reflective vest.  I started with the jacket's pit zips open (but mostly blocked by the vest).  I also had some arm warmers and an extra set of gloves in my bag.  Hindsight says I should have worn my winter boots, which are waterproof.  (Though maybe they would have made my feet too warm.)  Or, at least, two pairs of wool socks.

I started the ride at a carbo-loaded 208 lbs., only 1 pound less than the ride 3 weeks ago.  I've lost almost 40 lbs. since October, but very little in March, as I reduced my calorie deficit to keep from losing too much muscle mass along with the flab.  I'd never ridden this route before, but I rode the similarly hilly Urbana 200 at 228 lbs. last March, and hoped the 20 lbs. of weight loss would, if not getting me back to the midpack speed I had when I was riding every day, at least let me finish before dark.

My left knee started hurting on Urbana last March, and got bad enough on the Fleche last April that I had to abandon and then take time off the bike.  So I'd been paying close attention to it.  For this ride, I decided to raise my seat 3mm, as per the instructions for spring knee in Andy Pruitt's book.  And I brought Ibuprofin.  I figured that would be enough for 200k.

I did reasonably hilly 55-mile rides the previous two Saturdays, and a flattish 200k the week before that, so I was about as prepared as I was going to get, after a snowy winter of not riding much.  I figured I could go reasonably fast for about 30 miles then drag for the remaining 95, or go medium for about 50 miles then drag for the remaining 75.  The latter sounded smarter.  So I resolved to not chase the fast people early.

Driving to the start in Poolesville, I got to cross White's Ferry, then slow way down for the infamous speed camera.  Despite having to wait several minutes for the ferry, then drive at bicycle speed to make really really sure I wouldn't get a ticket, I still made it to the start in plenty of time.

30 people showed up.  About half of what we got for the previous ride, but considering the hills and the weather forecast, not bad.  No tandems, a sign of the hills to come.  We got one recumbent, though.

It wasn't raining (yet) at the start, which was nice.  I chose a small-print cue sheet because it fit on two pages and had the page break at a control, where I could theoretically flip the pages under cover and avoid soaking them.  This turned out to not be the best choice — while I could read the small print easily enough in the dry while stationary in good light, it was harder to make out the small print on a bouncing bike at dusk with water droplets all over the map case.  Next time I'll go with the big print version and deal with flipping the sheet an extra time.

We headed toward Mar-Lu slowly at 7 a.m.  I started in fourth position and stayed there, rather than sprinting to the front like an excited puppy.  Gradually the fast people passed me, and I stayed right where I was.  Eventually I felt like the group I was in was going too slow, and passed a few people, but I resisted the urge to go fast.  Not much point since I was going to go over Mar-Lu slowly regardless.  Approaching the light to cross Route 15, I remarked to someone that I'd never caught the green there and always had to wait for it.  Sure enough, the universe likes to prove me wrong and it turned green while we were a ways back.  So we sprinted for it.  I almost made it, but then it turned yellow and I started to brake, but then Bill (right behind me) kept sprinting so I re-accelerated and we both probably caught enough yellow to be legal.  Bill's a slow steady low-gear climber, so I decided to follow him up the hill at his speed, which turned out to be 5 mph on the lower 11% grade and 4 mph on the upper 15% grade.  Perfect.  We made it to the top, and I was happy to be breathing a bit hard but not exhausted, and then he shot away on the downhill at high speed, and I followed at a much lower speed, and didn't see him again for the rest of the day.

After Mar-Lu, the route went through Jefferson then turned toward Middletown.  Even though I hadn't done this ride before, we use the same roads on a bunch of others, so it all felt familiar.  The sky was very dark but it wasn't raining yet.  It was windier than expected, though.  I wasn't tired yet, but I knew the rollers of Burkittsville Road were each taking a bit out of me and I'd be paying later.  The first control was at the LDS (not the religion) store in Middletown at 25 miles.  I bought some Gatorade and some cashews.  It still wasn't raining, so I was a bit warm, so I took off my balaclava.

The next stretch featured Harmony Road, which is hilly.  And then Harp Hill Road, which gets to 18%, possibly the steepest hill on any DCR brevet.  (The switchbacks in Lost River State Park might be steeper, and that climb is certainly way longer, but that's on a ROMA ride.)  Steep enough that you need to lean forward to keep the front wheel planted.  I got passed by a pack of 5 riders at the bottom of Harp Hill, but stayed at my 3-4 mph pace rather than chasing.  It went up for approximately 5 million feet with a couple of false summits just to be mean.  My lower back started aching hard, something I don't remember happening before on a climb.  I saw a couple of riders stopped to rest near the top, and I really wanted to join them, but I knew that if I stopped it would be hard to get going again, so I gritted my teeth and kept pedalling.  Eventually I saw a couple of McMansions, the sign of an approaching summit, and started zipping up everything I'd unzipped to prepare for the descent.  Then the descent was surprisingly tame.  The descent on Wolfsville Road a few miles later was worse, because of potholes, but at least it was still dry.  The wind was really starting to gust.

Roger caught me from behind around mile 40, said hi, and blew on by.  I matched his speed for about 100 yards then realized it was a bad idea and dropped off.  (Roger likes to start brevets fashionably late then pass most of the field.)  I resumed my plodding pace into the wind.  I wasn't really hurting yet, so I was still in a reasonably aero position in the drops rather than in the fully upright position I'd need to use later in the ride.  My knee started aching around mile 45, so I pulled over to take a couple of Ibuprofin.  While I was stopped, a rider in an odd-looking helmet cover I didn't recognize passed me and greeted me by name.  I didn't recognize the voice (probably because of the wind) and had to speed up and get a good look to see who it was.  Once I saw around the helmet cover I realized it was George W., and rode with him for a while.  He was riding with kind of a burst-and-coast pace, while I was still in steady plod mode, so I passed him once to try to get my rhythm back, but then he re-passed me at the next stop sign and I just stayed behind him after that.  It was starting to rain (not hard yet) and he had fenders and I didn't so I didn't want to spray him.  We were both hurting a bit due to the hills and wind, and the ride wasn't even half over yet.

We reached the lunch control at Paul's Country Market in Waynesboro PA at mile 55, as the rain started to pick up.  Paul's is a Mennonite store with good deli sandwiches and baked goods and clean bathrooms.  Pretty much the perfect control, except that it's closed on Sundays so you don't want to ride this route then.  (Yoder's on the Old Rag brevet is very similar.)  I had a tasty roast beef hoagie (we had crossed the Mason Dixon Line so subs had officially become hoagies) and a pack of oatmeal raisin cookies.  I spent a few minutes eating and chatting with riders and volunteers, then decided to hurry out since I wasn't going very fast and might need the time later.

Unfortunately, I got turned around and headed down the wrong road.  Fortunately, volunteer Mike W. saw me going the wrong way and chased me down in his car and turned me around, so I only did 1.2 bonus miles instead of the at least 2 I would have done if I'd had to figure out my own mistake.  Still annoying, because I had to add 1.2 to every cue sheet distance for the rest of the day.  Back on course, I retraced the route back to Rouzerville, then went up Old Rt. 16 and Buena Vista, which went up a long long way.  Not steep, but far.  The climbing was annoying, as was the increasing rain, but I was happy to be halfway done with the ride and past (presumably) the 3 worst climbs.  It got really foggy up there on top of whatever mountain that is, and I was worried about half-blind drivers, so I made sure all my lights were on and prepared to bail off the road if needed, but luckily it wasn't.  The wind also got really ferocious without the side of the mountain to block it.  Crossed the Appalachian Trail, which meant it was time to go downhill again, and fortunately both the fog and the wind decreased away from the summit.

Spruce Run Road at mile 76 was a treat — steep, narrow, downhill, wet, and potholed.  I dragged my brakes most of the way down, alternating to avoid overheating either wheel.  Luckily there was no oncoming traffic so I was free to use whatever part of the road was the safest.  I got to a (different) LDS store at mile 79, but wasn't sure it was the right one at first.  It was.  I bought more Gatorade and some Golden Oreos (not as good as the cookies at Paul's but still a nice source of calories) and had a hard time getting the money out of my wallet to pay for them, as my hands were too cold and wet for fine motor control.  I swapped my gloves for dry ones (which would only stay dry for a few minutes).  George and Gary arrived while I was there, and I left before both of them.  Slow, but still not last!

Gary passed me a couple of miles later on Wolfsville Road.  Then we had to do Harmony Road again, but at least the return route bypassed Harp Hill.  (Going down the steep side in the rain would not have been much fun.)  George passed me pretty close to the 95 mile control at the Jefferson Crown gas station.  Not the most scenic control, but they had food and bathrooms, so good enough for me.  I got a Hershey's Moose Tracks cone that was surprisingly delicious; it was a bit cold for ice cream but I was feeling lethargic and wanted something with a lot of calories.  I took a couple more Ibuprofin for my knees (plural; my right knee was also aching a bit by this time).  George left right in front of me and we started up the less-steep side of Mar-Lu.  After Harp Hill and Buena Vista, it was really easy.  Then we had to go down the steep side, and it was wet, and I was very careful.  So was George, so I almost caught up with him again right after the light at US 15.  But he had more left in his legs than I did, and slowly drifted away into the distance, as we rode the nice flat(ish) section around mile 100.  That was the last time I'd see another rider until the end.

The rain was getting a bit harder, and my feet were getting cold.  I was happy that the ride was almost over.  Some quick (and probably questionable) math told me that if I could keep riding at 12 mph I'd finish in under 12 hours.  That seemed good enough, but not enough to really give me a sense of urgency.  I found myself using my small ring even on fairly flat roads.  Fingerboard Road had a bit of traffic.  Slate Quarry Road had some epic potholes.  As did Peach Tree Road, which featured an information control whose answer was "dumping."  Soon afterward, Peach Tree turned downhill, but it was so rough that it was still work rather than an easy coast to the end.  I pulled into Poolesville at 6:55 p.m., with an elapsed time of 11:55.  Really slow, but over an hour faster than Urbana last spring.

I ate 3 pieces of pretty good pizza at Cugini's while chatting with the other riders and volunteers.  The rain was steadily increasing outside, so I was glad to be done.  I hadn't remembered to bring dry clothes to change into, so after a while it started to get cold, and I headed to my warm car.  This was probably the hardest 200 I'd ever ridden, considering the hills and the weather.  Still only a 200, though, so not that hard in the grand scheme of things.

My knee held up okay, with only minor pains that were squashed with Ibuprofin. I was slow, but much faster than at Urbana last year.  I made it up Harp Hill.  My bike was mostly okay, though it autoshifted a couple of times, perhaps an indication that it's time to change the chain, cassette, and chainrings.  All in all, a pretty good day, though I was pretty grumpy for the last half of it.

We have a 300 coming in two weeks, a 400 in five weeks, and a 600 in seven weeks.  I'm cautiously optimistic about the 300, and worried about my ability to finish the others.  I don't think there's enough time to properly prepare.  But at least it should warm up before then.

Bicycles
Uncategorized

Comments (0)

Permalink

DC Randonneurs Wilderness Campaign 200k Brevet / Get Well Soon Lynn

A couple of weeks before our first 200k of the year, Lynn and Maile were hit by a psycho in a CR-V, while riding a permanent.  Maile is okay, but Lynn is still in the hospital and has a long way to go to make a full recovery.  This awful incident is a reminder that even the safest and most experienced cyclists are at the mercy of any drunk, distracted idiot, or nutjob in a motor vehicle.  In my opinion, the penalty for hit and run needs to be increased to the point where no even slightly rational person would ever consider doing it.  A decade in prison and lifetime revocation of driving privileges seems about right.  Fortunately, the vast majority of people out there are decent and kind.  Thanks to everyone who helped Lynn and Maile.

It's been an unusually cold and snowy winter, meaning a lot of cyclists have been riding less than usual.  (Of course the hardcore R-12 crowd finds a way to get a 200k in every month regardless of the weather, and thus don't have to beat themselves back into riding shape in the spring.  They might be onto something.  But it's so cold in January…)  I hadn't done a long ride since dropping out of the fleche with a knee injury last April, so I was worried about whether my knee would hold up for the full distance.  My warm-up rides were a hilly 100k back in early February (which I completed without drama but very slowly), and a couple of 35-milers since.  Not enough, but it would have to do.  I'd been dieting hard for months and had dropped from my bloated high of 245 lbs. down to 207 a couple of days before the ride, though I carbo-loaded myself back to 209 with a big dinner Friday night.  I also tweaked my serratus lifting on Thursday, but I didn't think that would matter much for cycling.

The forecast was for just below freezing at the 7 a.m. start and 50s by the afternoon.  So we needed to be prepared for a wide temperature range.  I wore shorts and a summer jersey, thermal tights and heavy winter jersey, cotton and heavy wool socks, summer mountain shoes with thermal shoe covers, lobster claws, a balaclava, and a reflective vest.  I also brought sunscreen, just in case it got hot enough to need to expose skin later, since I've gotten burned on winter rides in the past.  Plus arm warmers and lighter gloves.

We got a surprisingly big crowd for such a cold start, 58 riders, including a bunch I hadn't seen before.  I was freezing and rode off the front at about 20 mph in an attempt to warm up.  (I'm not sure whether this really works, since the extra effort warms you but the extra wind cools you.)  Most of the riders were a bit saner than me, so I got to break away for about a mile until Scott caught me.  We rode together to a red light, where about half the field caught us before it turned green.  (I didn't think to hit the pedestrian button, but Andrea did, and then it changed.)  That was the end of my time in the lead, and I tucked into the middle of the large lead pack for the next few miles, enjoying the reduced wind chill.  By the time we reached Nokesville at mile 4, I was starting to remember that I had no business going that fast, and started dropping back through the huge group.  It eventually split in half, and I was happy to be in the back half.  Soon enough I was split out of that group into a slower one, then an even slower one, and then I was riding by myself at 15 mph.  Honestly, a more reasonable speed for my current level of fitness.

Around mile 15, John and Cindy passed me on their tandem, with another rider in tow.  At first I figured I'd just let them go, but they weren't really going much faster than me, so I tucked in behind.  I was still cold, and less wind made things more comfortable.  Russ pulled in behind me, and we had a nice train with a tandem and 3 wheelsuckers for the next several miles.  Around mile 17, Russ got bored with our speed, and pulled away from the rest of the group.  That left 3.  A couple miles later, the rider in front of me decided the tandem was a bit too fast, pulled off to the left, then shot out the back.  That left me as the last surviving parasite.  We stopped briefly at the Elk Run store at mile 21.  I remembered I hadn't had any breakfast, and ate a Clif Bar.  I jumped back onto John and Cindy's wheel for a couple more miles, then decided that they were going too fast for me and slowly dropped behind.  I was happy to have finished the first 20% of the ride averaging 16 mph, since I knew I'd be much slower later.

We went through the very familiar low-traffic roads around Kelly's Ford.  The roads are a bit rough in places, but the pretty scenery and light traffic more than compensate for the bumps.  I plowed along at around 14 mph for a while.  At one point Dave S. caught me and we rode together for a bit, but then he made a pit stop and I got to ride alone again.  Just before reaching high-speed high-traffic VA Route 3 at mile 40, I decided to stop for a Gu packet and stow my balaclava.  In the minute I was stopped, 4 riders passed me.  More evidence that moving slowly is much faster than not moving.

The sugar and caffeine plus nearby fast traffic motivated me to speed up to about 18 mph for the 2.4 miles on Route 3, then I slowed right back down to 14 mph after exiting onto more bike-friendly roads.  I pulled into the first control at an Exxon in Locust Grove averaging about 15 mph (and dropping).  I wasn't really sure what I wanted to eat, so I grabbed some Twizzlers and Gatorade, figuring the unaccustomed sugar rush would keep me zipping along like a kid on Halloween.  (The Twizzlers were good, but I didn't finish the whole pack by the end of the ride, so my daughter got the leftovers.)  I also took off my shoe covers, but kept the rest of the winter ensemble on for a bit longer.  There was an older guy riding an adult-sized delta trike at the control, which put a smile on my face.  (Did I mention I saw a guy riding a penny-farthing on the W&OD Trail last weekend?  I thought he was on a huge unicycle at first, until he came close enough for me to see the frame and back wheel.)

The 3 miles from the control to Wilderness Battlefield were on VA 20, another busy highway, but not as fast as VA 3.  I rode them at normal speed rather than getaway speed this time; my turbo boost was already exhausted for the day, less than halfway through the ride.  The entrance sign to Wilderness Battlefield was very welcome, as it meant a few miles with pretty trees and no traffic.  There was a lot of snow remaining in the woods, but none on the road, even in shady areas, so it was nice easy riding.

The 9 miles between Wilderness and Spotsylvania Battlefields on Brock Road are already a blur.  Some hills but nothing difficult, some traffic but nothing horrible.  Your basic filler section.  The ride because memorable again once it entered Spotsylvania Battlefield, which features the usual pretty woods, historical signs about salients and wounded officers, and quaintly non-standard road signs of Virginia battlefields.  The battlefield featured the first information control of the day, and while I reading the sign, a new rider showed up, giving me a chance to cross-check my answer (never want to be disqualified due to inability to read a sign correctly) and him a chance to borrow my pen.

After leaving Spotsylvania Battlefield it was only a short distance to the halfway control in Spotsylvania.  It involves crossing a couple of lanes of busy highway to make a left turn, but there was an extremely courteous pickup-truck driver who slowed way down to let me over, which made it easy.  I wasn't very hungry, and was concerned about how slowly I was riding and whether I'd finish before dark, so I decided to stop at 7-11 rather than a proper lunch spot.  I bought some more Gatorade and a slice of 7-11 pepperoni pizza.  It wasn't *good* pizza, but it didn't make me sick either, and it was only about a dollar.  I chased it with a couple more Twizzlers from my earlier purchase, removed my long-sleeve jersey, swapped lobster claws for light gloves, pulled the bottom of my tights up so they covered my knees but not my shins, put on some sunscreen, and took off having spent only about 10 minutes at the control.

I needed all the time I saved, as even after lunch kicked in, I wasn't very energetic and was still plodding along at 13-14 mph.  The section after Spotsylvania isn't very exciting, so I spent a lot of time staring at my cue sheet and odometer to make sure I didn't start daydreaming and miss a turn.  I got passed by three riders, but didn't bother trying to speed up and hang with any of them.  My halfway-pulled-up tights were bothering me, and my feet (which still had two pairs of socks) were getting warm, but I saw there was another information control approaching and decided to wait until I got there rather than making an extra stop.  I reached the Chancellorsville Battlefield information control right behind another rider, and then Ed and Mary came in on their tandem right behind us.  I stripped off my excess socks and tights, applied more sunscreen, and left while everyone else was still chatting.  I remembered running out of energy last year around this spot, and wanted to give myself a nice head start so they wouldn't catch me for a while.  Maybe by then I'd have more energy and would be able to latch on.

I rode through the very nice section around Kelly's Ford hoping the energy would appear.  Nope.  Still 13-14 mph.  After about 8 miles the tandem did catch me, but I was in no shape to chase them and just said hi as they sailed by.  I stopped briefly at Myers Grocery at mile 93 to get a Vanilla Coke (for the caffeine, plus I wanted something different after drinking several liters of Gatorade) and 270 glorious calories of kettle-cooked Jalapeno potato chips.  (Mmmm, salt.)  Two cyclists sailed by while I was eating, and two more came into the grocery while I was there, proof that, no matter how slow I felt, there were still others around my pace.

The century mark felt like a significant achievement (both because I hadn't ridden a century in almost a year and because it meant the ride was over 75% done), and from then on I started treating every 5 miles ridden as a milestone.  That kind of numerological silliness shouldn't be necessary on a 200k, but my knee was starting to ache and I was trying to keep my mind off it.  There were snowmelt puddles here and there along the road, and every time I hit one, I got noise and splashes from my front wheel, like my brake was dragging a wee bit on a high spot on the rim, only enough to notice when the rim was wet.  I didn't think it was worth stopping and trying to true the wheel since the drag, if not completely imaginary, was very slight.

The 107-mile control at Elk Run snuck up on me — I'd forgotten about it, until I flipped to the bottom half of my cue sheet, and there it was.  One of my rules is to always eat at the penultimate control, because bonking in the last 10 miles of a ride is embarrassing, and I've done it a couple of times.  So I had my first ice cream of 2014, a small cup of Hershey's Dulce de Leche, which was pretty great by the standards of things you eat with a disposable wooden spoon.  The 3 other cyclists who were at the control left before I was done, and I probably couldn't have kept up with them anyway, so I was happy to push off at my own slow pace.  My primary goal was to finish, my secondary goal was to finish before dark, and my tertiary goal was to finish faster than last year.

The miles from Elk Run to Nokesville were on flat and reasonably low-traffic roads.  But there was a small but noticeable headwind much of the way.  At least that's what I told myself, to justify the 12s and 13s I kept seeing on my speedometer.  There was a brief stretch on Hazelwood Drive where I caught a tailwind and zipped along at what felt like a decent clip, but that only lasted a couple of miles and then I had to turn into the wind again.  I reached Nokesville, and followed a car through the light at 28 (I remembered the sensor not picking up my bike last time), and then it was only about 5 miles to the finish.

Unfortunately my cue sheet ended at mile 127.5, meaning I needed to stop and flip it to see the last turn or two.  This annoyed me way more than it should have.  I was pretty sure I needed to make a right on Sudley Manor and that would take me to the strip mall with the finish, but not sure enough to not check, so I pulled over to confirm it.  And, while I was stopped, I turned on all my lights since it was less than an hour from dusk and some of the cars had their lights on.  Sure enough, that was my right turn.  Oh well.  I finished in 10:20, 14 minutes faster than last year but 83 minutes slower than two years ago (when I was bike commuting 100+ miles per week).

I had two slices of post-ride pepperoni pizza.  I also had half a cookie and some other sweet baked thing I can't remember.  More than I needed; with all the Gatorade, this was definitely a calorie-surplus ride.  I chatted with people for a bit, but then started getting cold (I was still in shorts and a summer jersey, comfortable for riding in 60F temperatures, but not for sitting around) and headed for the warmth of my car.  My knee wasn't hurting that badly, so I considered the ride a success.

After riding the Wilderness Campaign 200 for three years in a row, it's still not one of my favorites.  I like the lack of climbing in the first 200 of the year, as it lets riders ease back into shape.  I like the start/finish location in Bristow.  I don't like the heavy traffic on several roads.  The battlefields are scenic, but because they've got trees overhead and don't get much traffic, they're always a threat to be snowy even if the rest of the route is clear.  Overall, I think I prefer Tappahannock for an early-season flattish ride.  Though that's farther away from most of our riders, so we'd probably get a worse turnout.  Tough call.

Only 3 weeks to get ready for the next 200, Paul's Paradise, which includes some actual climbing.  Modest goals: don't get hurt, finish within the time limit, and ride rather than walk up Harp Hill.  (Mike W. said it's 18%.  I really should ride the bike with the triple.  But I probably won't.)

Bicycles

Comments (0)

Permalink