2015-03-06
Our brute force solution for port isolation without port isolated switches
We have a problem: a number of our areas don't have anywhere near the network jack density that people need. Sometimes this is easy to deal with; if everyone in an office just needs one particular sandbox network we can just deploy a dumb switch to add more ports for it. But sometimes people need either multiple networks in one office or, more crucially, more ports on one of our port isolated networks, which we really want to stay that way (even between machines in the same office). And that's the real problem, because we just haven't found any reasonably priced fanless small switches that have good support for port isolation (as well as VLANs; we need both in the same switch).
(Some offices are used by only a single person so it's only sort of dangerous if their infected laptop starts attacking their desktop. But other offices have multiple people, for example grad students, and there we really don't want person A's laptop trying to infect person B's laptop.)
So we've adopted a brute force switch intensive solution to the problem. The 8-port fanless switches we use may not support port isolation but they do fully support VLANs, so we put together what is basically a virtual patch panel system. Each port on an office switch is in a separate VLAN, all of which are forwarded over the uplink port. In the wiring closet, this uplink plugs into another switch that then breaks out each of those VLANs to a separate port again. We then plug each port on the wiring closet forwarding switch into an appropriate port in our regular network infrastructure for whatever network is needed. Effectively the wiring closet switch is a patch panel; it patches through its network ports on a 1 to 1 basis to network ports in office(s).
We initially considered a more complicated and obvious version of this where the wiring closet forwarding switch carried some of our non port isolated VLANs as real VLANs (passed to the office switches) and only did the 1:1 port patch panel thing for the port isolated networks. After trying to plan it out we decided that the potential moderate improvement in port density on the forwarding switch simply wasn't worth the extra complexity, including in keeping track of what ports were on what VLAN(s), what exact configuration a replacement 8-port switch would need for any particular office, and so on. Pure port forwarding has the great virtue of being completely generic.
(Doing actual VLANs on the office switches and the port forwarding switch might slightly improve overall network bandwidth if office machines talk to each other on non port isolated networks. In practice this is not the usage pattern on most of our networks; if there are office machines, they're usually talking to servers that are located elsewhere.)
We could have used more 8-port switches as the wiring closet switches, but for various reasons we wound up deciding to use 24-port ones instead (partly because our 24-port switches are designed to be rack mounted while the little 8-port ones generally aren't). A 24-port switch neatly supports three 8-port office switches with no wasted ports, but it does mean we have three varieties of 8-port office switches (one for which set of seven per-port VLANs it uses). In practice this is not a problem; we label the 8-port switches based on whether they are set up for A ports, B ports, or C ports. Then you just make sure you always connect an A port switch to the A port uplink on the 24-port switch.
(And if you get it wrong you find out right away because nothing works since the VLAN numbers don't match up. The 24-port switch is sending the 8-port switch traffic for (eg) VLANs 1 through 7, while the 8-port switch expects and is sending traffic for VLANs 15 through 21. Both sides drop each other's unexpected VLANs and nothing gets through.)
The simple way CPython does constant folding
When I looked into CPython's constant folding as part of writing yesterday's entry, I expected to find something clever and perhaps intricate, involving examining the types of constants and knowing about special combinations and so on. It turns out that CPython doesn't bother with all of this and it has a much simpler approach.
To do constant folding, CPython basically just calls the same general
code that it would call to do, say, '+
' on built in types during
execution. There is no attempt to recognize certain type combinations or
operation combinations and handle them specially; any binary operation
on any suitable constant will get tried. This includes combinations that
will fail (such as '1 + "a"
') and combinations that you might not
expect to succeed, such as '"foo %d" % 10
'.
(Note that there are some limits on what will get stored as a new
folded constant, including that it's not too long. '"a" * 1000
'
won't be constant folded, for example, although '"a" * 5
' will.)
What is a suitable constant is both easy and hard to define. The easy
definition is that a suitable constant is anything that winds up in
func_code.co_consts
and so is accessed with a LOAD_CONST
bytecode instruction. Roughly speaking this is any immutable basic
type, which I believe currently is essentially integers, strings,
and floating point numbers. In Python 3, tuples of these types will
also be candidates for taking part in constant folding.
At first this approach to constant folding seemed alarming to me, since CPython is calling general purpose evaluation code, code that's normally used much later during bytecode execution. But then I realized that CPython is only doing this in very restricted circumstances; since it's only doing this with a very few types of immutable objects, it knows a lot about what C code is actually getting called here (and this code is part of the core interpreter). The code for these basic types has an implicit requirement that it can also be called as bytecode is being optimized, and the Python authors can make sure this is respected when things change. This would be unreasonable for arbitrary types, even arbitrary C level types, but is perfectly rational here and is beautifully simple.
In short, CPython has avoided the need to write special constant folding evaluation code by making sure that its regular evaluation code for certain basic types can also be used in this situation and then just doing so. In the process it opened up some surprising constant folding opportunities.
(And it can automatically open up more in the future, since anything
that winds up in co_consts
is immediately a candidate for constant
folding.)
Sidebar: What happens with tuples in Python 2
In the compiled bytecode, tuples of constant values do not actually
start out as constants; instead they start out as a series of 'load
constant' bytecodes followed by a BUILD_TUPLE
instruction. Part of
CPython's peephole optimizations is to transform this sequence into a
new prebuilt constant tuple (and a LOAD_CONST
instruction to access
it).
In Python 2, the whole peephole optimizer apparently effectively doesn't
reconsider the instruction stream after doing this optimization. So if
you have '(1, 2) + (3, 4)
' you get a first transformation to turn the
two tuples into constants, but CPython never goes on to do constant
folding for the +
operation itself; by the time +
actually has
two constant operands, it's apparently too late. In Python 3, this
limitation is gone and so the +
will be constant folded as well.
(Examining __code__.co_consts
in Python 3 shows that the
intermediate step still happens; the co_consts
for a function that
just has a 'return
' of this is '(None, 1, 2, 3, 4, (1, 2), (3, 4),
(1, 2, 3, 4))
', where we see the intermediate tuples being built before
we wind up with the final version. In general constant folding appears
to leave intermediate results around, eg for '10 + 20 + 30 + 40
' you
get several intermediate constants as well as 100.)