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

DC Randonneurs Glen Echo 106k Populaire Ride Report

DC Randonneurs' first ride of 2014, a mere 106 km (66.2 mile) populaire, was scheduled for January 25.  It snowed.  They wisely rescheduled for February 1.  It didn't snow; it was just cold.  Not even horribly cold, compared to what we'd had lately; the forecast said about 28F at the start and 50F at the finish.  I hadn't done an organized ride since I hurt my knee on the Fleche last April, and I hadn't ridden at all in January (because it was cold and lack of bike commuting has turned me into a wuss), but I figured even I could handle that.

It was the same route as last year: start at Glen Echo Town Hall, west on MacArthur to River Road, then west on River Road for about 10 miles, then various back roads to the halfway control in Hyattstown.  Then other back roads to the 2/3 distance control in Poolesville.  Then back roads to River Road, Glen Echo, and pizza.  All very pleasant, except for River Road, which is a popular bike route despite being infested with rollers and cars.

We got 36 people, not bad at all considering the cold weather and the postponement.  I thought about what to wear for a while, then went with the winter boots and winter helmet, figuring I was not acclimated to the cold.  I also had tights, my thickest jersey, a light balaclava (am I the only person who owns 3 different balaclavas?), and a reflective vest, but those could come off if I got too warm.

Made it to the parking lot at Glen Echo Park (a nice little park run by the National Park Service, with a historic ballroom and an old carousel plus some trees).  I was about an hour early, so I rode over to the park to check out the carousel (which I vaguely remember riding as a kid 30-some years ago) and use the bathroom.  Then I meandered over to the Town Hall for the start.  There were signs saying to take off bike shoes to avoid scuffing the floor, but the organizers had thoughtfully put down cardboard to make this unnecessary.  I got my brevet card and some food, then still had time to kill, so I went back to my bike until I got cold, then took a seat downstairs where I could chat with people without being in the way.

About 15 minutes before the start I decided it was time to stop cowering and start getting used to the cold, so I went out and double-checked my bike.  We got a warning about bad road edges due to construction, and to watch out for leftover snow and ice, and then it was time to go.  Unfortunately, my winter boots are much harder to clip in than my summer shoes, so it took me multiple tries to get my left foot in for the uphill start.  Fortunately, this didn't cause anyone behind me to crash.

I started off near the front, like I always do, but knew I wouldn't last there.  So I just settled in at a gentle pace and let everyone gradually pass me.  The first few miles of the route are pretty flat, but the first decent hill told me that I didn't have my climbing legs.  I'd done heavy squats the day before, but I think the lack of riding for the last month had more to do with it.  Luckily, 8 hours to do 106k meant I didn't need any climbing legs.  As long as I didn't stop and take a long nap, I'd make it.

We had an information control at mile 14, right after leaving River Road.  Surprisingly, I was still wearing everything I started the ride with, even though it had warmed up to a bit above freezing.  My pen (which is the kind with 4 different colors, so that if one runs out of ink I have 3 backups) didn't work, probably because of the cold, but I had a golf pencil for backup.  Right after the info control, we turned onto Montevideo, which is an extremely small road by just-outside-the-Beltway standards — well-worn pavement, lots of shade, very little traffic, and thus the first unmelted snow and ice of the ride.  I was paying attention and dodged it easily enough.

A couple miles later, my head started getting warm.  And my helmet felt super-tight.  So I briefly stopped to remove my balaclava, solving both problems.  Of course, this happened just a couple miles after the info control so that I could make an extra stop.  No big deal, since I was riding alone and a minute didn't matter.  I just objected to the inefficiency on general principles.  I partially unzipped my reflective vest and my winter jersey but decided it wasn't quite warm enough yet to remove either one; I would pull those zippers up and down a few inches on most of the big downhills and uphills for the rest of the day, giving me at least the illusion of a bit of temperature control.

A pack of five more riders passed me right before the halfway control to remind me that I should ride more in the winter.  Then I reached the stop at Denise Bakery and Deli in Hyattstown.  There were a bunch of people there eating real food, but I wasn't that hungry and wanted to get going again, so I just bought a Cherry Coke (with HFCS for extra calories, my first non-diet soda in months) and got my card signed.  There was a bit of drama because the brevet card said the control closed at 10:24, making most of us still at the control disqualified by a few minutes, but we figured it was a typo.  (It was; someone later noticed that the cue sheet had the correct time of 11:24.)  I ate a Clif Bar for some more calories, swapped my lobster claws for my lighter full-finger gloves, and got back on the bike.

The 14-mile section between controls was very pleasant, with Peach Tree and Comus and Barnesville and Cattail all being reasonable cycling roads without too much Saturday morning traffic.  I got passed by the same group of 5 riders (who were riding a bit faster than me but controlling a bit slower), but reached Cugini's Pizza in Poolesville in good spirits, probably buoyed by the sugar and caffeine.  I decided to repeat my successful meal experiment with another Cherry Coke (identical to the previous one but costing about a quarter more — I guess Poolesville is the posh part of western Montgomery County), and headed out without getting any pizza.  I knew there'd be pizza at the end of the ride when the clock would no longer be ticking.

The 7 miles from Poolesville back to River Road were fine, but I was definitely slowing, despite the temperatures warming into the high 40s.  Clearly the fault was with my lack of endurance, not the weather.  My attention wandered at one point and I found myself going right over a patch of unmelted snow that I didn't see until it was too late, but it wasn't that slippery and I was going straight so there was no drama.  River Road was once again hilly and once again somewhat full of cars, but most of them were pretty polite and gave me a wide berth.  (The double-yellow line has noise grooves in it to prevent head-on collisions, and I got used to hearing the noise of cars crossing it to give me more space.)  It seemed like that road went on forever, but just as it made the transition from semi-rural to suburban, the turn onto Persimmon Tree arrived.

The last few miles of the ride were easy.  3.5 miles on Persimmon Tree, a mile and a half on MacArthur (quite torn up by construction but still fine if you took the lane), and then done.  My time was 6 hours, an embarrassing 11 mph.  That's a 600 km pace, including a sleep stop, and there was no sleeping on this ride.  So clearly I have a lot of work to do this spring to get the pace up.  But no injuries, no mean dogs or really bad drivers, nice weather (no mean feat this winter, which has been very cold and pretty snowy), and a chance to see a bunch of friends I hadn't seen in a while.

Next ride is the Wilderness Campaign 200 on March 8.  I did it in 8:57 two-years ago (in hardcore year-round bike commuter shape) and 10:34 last year (in soft telecommuter occasional weekend cyclist shape).  My modest goal is to finish faster than last year.

Bicycles

Comments (0)

Permalink

Why I Just Closed my LinkedIn Account

So I just got an email from LinkedIn saying that someone wanted to connect. About half of these are spam from recruiters who I have no connection to, and the other half are actual people I've worked with. This one was an actual person who works on the same open source project as me, so I added him.

And then the LinkedIn site said (roughly) "Add your email password! So we can manage your contacts for you! It's secure (picture of padlock)."

Ahem:

1. This is phishing. You should never give your email password to any site (except your actual email provider, since you need it there to login). Your email password is the key to your entire online identity — if someone has your email password then he can, for example, look for emails from your bank to know which bank you use, then reset your online banking password and loot your bank account. (Of course LinkedIn is not actually planning to do that — but a rogue employee or someone who hacks into their systems might.)

Of course I'm not stupid enough to give them my password, but many people are. It's ridiculously irresponsible for them to ask for it.

2. Secure my ass. LinkedIn leaked 8 million users' passwords less than a year ago, because they were storing them in the database unsalted. Which is seriously negligent. They've probably fixed that particular bug, but there are probably plenty more.

3. They should know better than to put their marketing plans ahead of their users' security. They're not going to learn about security until it costs them users. So, scratch one user.

Rant
Security

Comments (0)

Permalink

Team Blues Fleche Report

I'd never ridden a fleche, because it had always conflicted with my daughter's birthday. This year the fleche was a week earlier, finally giving me a chance to ride it, so I started looking for a team. (Then my wife moved my daughter's party up a week until it conflicted with the fleche, but at that point I was committed. Can't win.) The first team to accept me was Team Blue, captained by RBA Nick and featuring George, Christian, and Dave. I'd ridden with all of them before, and I knew Nick was super-organized and had a well-tested route, so the only problem was riding 233 miles in 24 hours.

After losing my bike commute in September, not riding enough all winter, and being very slow in both 200 km brevets I'd ridden in the early spring, I was pretty worried about finishing in time. Luckily fleche pace is pretty slow, a bit under 10 mph, so as long as I kept pedaling I should be okay. I did a bunch of short rides between the Urbana 200 and the fleche, but still felt undertrained. And I was a bit worried about my right knee, which had been sore since Urbana. I had figured it was just spring knee from ramping up mileage too quickly, but the pain was on the outside like ITBS. It wasn't that bad the week before the fleche, though, so I packed some Ibuprofin and figured I'd just deal with it.

The weather forecast said it would be dry, with lows in the mid-30s and highs in the mid-50s. Not bad for early April. It was about 39 degrees at 5 a.m. at the 7-11 in Arlington when we started the ride. Warm enough that I didn't put on my jacket or shoe covers. Dave agreed and said he was getting a bit warm a few miles into the ride, so I thought I got it right. But then it started getting colder, as we descended into the moist valley between Reston and Vienna on the W&OD trail. I didn't really feel like stopping to put on more clothes, though, so I just lived with being chilly, as the temperature dropped to 34 degrees. Luckily it was still above freezing, so the puddles in low spots were water not ice. Having commuted on the W&OD trail for years, I enjoyed not having to worry about navigation for once, just deer and early-morning joggers.

As we approached the end of the trail in Purcellville, we were a few minutes behind schedule. Nick didn't want to speed up and burn energy early in the day, so instead we decided to control at the 7-11 instead of the McDonalds, and try to get through the control fast. But that ended up not happening, and impatience split the group, as Nick and George took off, followed by Dave, then me, as Christian was still putting his gloves and helmet. I saw Dave up the road and sprinted to catch up, but Christian was out of sight, so I rode to the front to ask George to slow down a bit, then we waited for Christian to catch us. He eventually did, though not until Nick and I were starting to worry that he had a flat or something, and I dropped back to ride with Christian and make sure he was okay. Unfortunately we soon missed a turn while chatting and rode a bonus mile, not what you want to do when you're behind.

Nick took a pit stop, so we caught him, and I decided to ride ahead to catch Dave and George and make sure they weren't too far ahead of us. But I heard a clicking from my rear wheel on a downhill, and stopped to diagnose the problem. One of my seatstay-mounted taillights was a bit loose. Two of the pre-ride rules I've learned through painful experience are not to mess with the bike the night before a long ride, and to replace taillight batteries before a long ride. Unfortunately it's impossible to obey both of these simultaneously: it appears that when replacing the batteries I dislodged the taillight a bit.

My initial hurried taillight tightening didn't stick, and when the light came loose again I got off the bike, ate a Clif Bar, took off a layer of clothes, and tightened the light better. This put me behind everyone, so I burned some energy riding back quickly. At that point I was approaching Middleburg alone. I made another wrong turn (626 north is two blocks away from 626 south, not directly across from it like you'd expect if highway design made sense) and rode another 1.2 bonus miles, then corrected my course and got onto Halfway Road toward the plains farther behind my teammates and more annoyed at myself. I rode harder, and eventually saw Nick and Christian stopped ahead next to a closed store. I pulled in next to them to remove the taillight that had just come loose yet again, intending to reinstall it before nightfall. (Though I always use two taillights in case one fails, so I'd be okay even without doing so.) Christian had just told Nick that his hip injury from last year's Cascade 1200 had come back, and that he needed to abort the ride and head back for Arlington before he got too far away to do so. We were about 60 miles into the ride. Nick gave me a Ziploc bag to keep the little taillight hardware from getting lost under all the jujnk in handlebar bag, and we said goodbye to Christian and resumed riding, hoping that George and Dave had the patience to wait for us at the 75-mile control. We continued down Halfway (very pretty road, with occasional traffic) to the Plains, then east on VA 55 (fast two-lane highway, moderate traffic) for a few miles, then off south toward Warrenton.

When we got to the Sheetz they were indeed waiting for us. They said they'd only been there for a couple of minutes, because they'd stopped briefly in The Plains and sent Nick a text (which Nick didn't notice). We all grabbed some food; mine was a large yogurt concoction, which Dave said didn't look like enough lunch for me, but it was really only elevensies not lunch, and I had lots of Clif Bars and Gu packets on the bike if I got hungry before the next stop.

The four of us rode together through the hilly old downtown part of Warrenton, which was a bit crowded, and then down nice suburban highways. George had been leading the whole previous section and didn't want to set the pace for a while, so the other three of us switched off. My previously-injured knee started to ache a bit, and at noon I stopped for a second to take two Ibuprofin. Traffic was light and friendly, until out of nowhere a crazy SUV came flying past us at about double the speed limit, despite an oncoming car. I was in third position and slowed down, fearing that the psyco would swerve right into us to avoid the head-on crash. Luckily the oncoming car had a good driver, who stopped (despite having the right of way) to give the psycho more room, and he made it around everyone without impact. But rather than continuing on his deranged way, he stopped and waited for us to catch up so that he could scream at us for a while. Dave backed way off, fearing that the guy might turn around and try to run us over, while Nick and George discussed the Virginia highway code with the nut job, and I memorized his plate number in case he ran one of them over and I needed to call the cops. Luckily his passengers talked him down and he eventually left without any real harm, but a nut case with a two-ton weapon can ruin the mood even if he doesn't actually hit you.

So we watched carefully for the psycho in case he came back at us and we needed to bail off the road, and discussed the multi-time drunk driver who killed our friend Stan a couple of years ago and was up for parole. The only silver lining was that my knee had stopped hurting; either the Ibuprofin from earlier had finally kicked in or the adrenaline from almost being run over was helping. We made it to the 104-mile control at Tolliver's Grocery / Sonny's BBQ, and everyone ordered a pork barbecue sandwich. Unfortunately there were a bunch of customers and only a few people working there, so it took a long time to get our food, and while we waited my knee tightened up again. The sandwich was good, though. I did some stretches to try to loosen up my knee, and then everyone needed to use the bathroom but the store didn't have one, so we all headed off down the road in search of secluded trees.

When we all got on our bikes again after the nature break, George had broken away. The other three of us rode at moderate pace while digesting, and didn't see him for a long time. Nick needed to stop to adjust his bike, but I told him that since I was worried about my sore knee making me slow, I'd keep going. He agreed and then we were all split up, with George then me then Dave then Nick all out of sight of each other. Dave eventually caught me as I approached Reva, and we saw George waiting ahead on the porch of a closed store. I got off my bike to stretch my knee, and after Nick didn't catch us for a few minutes, George called him. It turned out that Nick's chain had broken. He asked us to wait for him. But then George suggested that I go ahead since my knee was slowing me. I did so, and rode ahead pretty slowly. Eventually the others caught me, but then Nick had to stop again to adjust his bike's shifting, and asked George to stop with him and Dave and me to keep going. My knee was warmed up and feeling a lot better, and there were no steep hills for a while, so I started riding faster, until Dave told me to slow down so Nick and George could catch us. At that point I was much more optimistic that I could finish, though a bit worried about Nick since he kept having mechanical problems.

Nick and George finally caught us, and Nick mentioned that he thought I was rocking my hips. I was pretty sure my saddle height was good, but it was possible that I was using a different position to favor the sore knee, so I lowered my saddle a couple of millimeters. It didn't really help, though. As we approached the 125-mile control, it started hurting again. We stopped for dinner at a McDonalds in Madison. I had a Southern Chicken Sandwich (basically McDonald's imitation of Chick-Fil-A) and a vanilla shake. While we were there, Nick got a call from Christian, who'd made it back to his car in Arlington.

The next 24 miles got progressively worse for me. The road was a bit rough, and there were some hills, and the knee got worse, and I started falling off the back of the group. George noticed and dropped back to ride with me and provide encouragement. I did my best to tough it out, but about mile 140 I decided there was no way I could finish the ride in time, and that if I kept trying I would slow down the team and possibly cause them to miss the time limit. So I continued to the mile 148 control at Baker's Store, then told the team I was DNFing, and tried calling for a ride. My wife was hosting my daughter's sleepover, so she couldn't come pick me up, but my parents could. My cell phone was acting flaky, so I ended up using George's phone to call them. They weren't home yet so I left a message and decided I'd continue along the course, slowly, aiming for a nice spot to be picked up like the Inn at Kelly's Ford (mile 165) or M&P Pizza (mile 171), if I could make it that far.

While I was making phone calls, the Hamid's fleche team, which was using the same route as us but started an hour later, rode into the control. We chatted a bit with them, then rode off before they did. My knee was pain-free for about half a mile, and I briefly had delusions of finishing the ride, before I remembered that I hadn't bothered getting my card signed at the last control so I was a DNF regardless. A bit later, the knee pain returned. A couple miles after that, my phone rung, and I said goodbye to the team so I could arrange a ride.

While I was on the side of the road chatting, Hamid's team rode by. They asked if I needed help but I waved them on, since none of them seemed to have a bag big enough to carry a motor vehicle inside. My parents were able to pick me up, but we had to arrange a rendezvous point and I'd rather ride slowly toward a better landmark than sit on the side of a highway getting cold. So we agreed that I'd ride to the Inn at Kelly's Ford, and if they got there before me they'd call again. They ended up calling again when I was in Lignum, about 10 miles from the previous control. I saw the closed but well-lit Lignum Main Post Office ahead, and asked them to meet me there. It was fully dark by then, and getting cold, so once I got to the post office I put on all my warm clothes, and paced back and forth rather than sitting on the cold metal bench.

George and Nick and Dave did manage to finish the fleche in time, and 3 riders is enough to count as a team finish, so bravo to them. I wish I'd been able to finish with them, but dropping out was clearly the right decision. Stairs were really hard for a day, and the knee's still sore. I'm pretty sure it's ITBS, which I had in the other knee after a long off-road ride years ago. The treatment is rest and some specific stretches to loosen the tendon on the outside of the knee. So no long rides for me for a month or two. I'll certainly enjoy volunteering the 600 more than I would have enjoyed riding it.

Bicycles

Comments (0)

Permalink