The Harsh Perils of Assumptions

Why trust is overrated when you're stuck
Published on 2024/05/29

I don't think you need to have been coding that long before you find your first seemingly unsurmountable bug or issue. This is why my daily thought will be reliable, independent of your seniority. We probably all learned this the hard way, but I had a nice reminder for it just today.

When you work head down on something, there are several functionalities you might take for granted. Part of it might be due to trusting whoever wrote the existing code before you (which might still be you!), part of it might be relying on someone else's experience and/or explanations, and part of it might be due to distraction. So you go about your day until you get stumped. Today I was tackling a performance issue, an itch I wanted to scratch for a while that I finally got around to. At a high level here's the go code I was dealing with.

type myType1 struct {}
type myType2 struct {}
type myType3 struct {}
type myType4 struct {}
type myType5 struct {}
type myType6 struct {}
type myType7 struct {}
type myType8 struct {}
type myType9 struct {}

type (
  myInt    int
  myLong   int64
  myDouble float64
)


func tryConversionToMyType(input interface{}) (interface{}, bool) {
  switch v := input.(type) {
  case myType1:
    return v, true
  case myType2:
    return v, true
  case myType3:
    return v, true
  case myType4:
    return v, true
  case myType5:
    return v, true
  case myType6:
    return v, true
  case myType7:
    return v, true
  case myType8:
    return v, true
  case myType9:
    return v, true
  case myInt:
    return myInt(v), true
  case myDouble:
    return myDouble(v), true
  case myLong:
    return myLong(v), true
  default:
    return nil, false
  }
}

interface MyValue {
  Export()     interface{}
  ExportType() reflect.Type
  // many other methods
}

func mySlowFunction(input MyValue) (interface{}, err) {
  // ...a lot of other stuff
  if convertedVal, ok := tryConversionToMyType(input.Export()); ok {
    return convertedVal, nil
  }
  // ...more stuff
  return nil, errors.New("uh oh")
}

The first thing I did was to benchmark this. I already knew it would crumble when the input is a very large slice, so I focused mainly on that. Before getting there, I had to figure out which part was slow inside mySlowFunction. This was easy to test so while in other scenarios I would profile it, I just added a few logs. I quickly isolated the issue to this conversion function but when I looked at the body of tryConversionToMyType I was puzzled. I don't recall ever stumbling upon a slow type check. Maybe I never tested this with very large slices? I was dubious but, the larger the slice, the longer it would take to run it.

To test my theory I guarded the execution of that function with a different type check, this made it possible that large slices would not even get inside tryConversionToMyType. That worked nicely giving me a 600x improvement. Not too bad. But something didn't sit right with me. Will I have to do specific type checks again in the future based on the type of the input? How sustainable will that be?

Knowing that I might be making some wrong assumptions, I recreated this scenario differently. Instead of using a variable of type MyValue, I provided a very large slice of interface{} directly. I removed my special guard and the 600x gain stayed. As I suspected, the type switch was not the problem at all (we're talking microseconds). I got distracted by that input.Export() and made the wrong assumption. Ideally, I should have made none but it would have been wiser to not second guess that a type switch could be performing poorly well for large slices, it also makes no sense for it to be that way.

In the end, the ExportType function gives me enough to refactor the tryConversionToMyType to check based on reflect.Type thus avoiding the expensive call to Export.

Thoughts

Question everything!

If there's one thing I want you to take away from this is to revisit your assumptions when you're either puzzled or stuck. Write them down, say them out loud or, better yet, discuss them with a teammate. This is why reviews are so powerful, people come in with different assumptions than yours helping you rationalize every bit of information. The sooner you can do this exercise on your own the better. It might come with experience (it did for me when I got bit much worse than this) but nothing stops you from building that habit right now.

0
← Go Back