I'm looking at changing my VPS provider from
DigitalOcean to
Vultr. These were the top two contenders
when I initially chose a provider. I can't fault DigitalOcean's
service, but Vultr have better pricing and give more control over
DNS details such as
PTR
records.
I have the configurations ready to go, so I suspect I'll make the
move over the next few days. I'll be taking this opportunity to
enable IPv6 for the http
and smtp
services. Expect outages!
A little known feature of javac
is that it will inline constant
references when compiling code. This can mean that it's possible to
accidentally break binary compatibility with existing clients of a
piece of code when changing the value of a constant. Worse, tools
that analyze bytecode have no way of detecting a binary-incompatible
change of this type.
For example, the following class defines a public constant called NAME
:
public final class Constants { public static final String NAME = "com.io7m.name"; private Constants() { } }
Another class refers to NAME
directly:
public final class Main0 { public static void main( final String args[]) { System.out.println(Constants.NAME); } }
Now, let's assume that NAME
actually becomes part of an API in
some form; callers may pass NAME
to API methods. Because we've
taken the time to declare a global constant, it should be perfectly
safe to change the value of NAME
at a later date without having
to recompile all clients of the API, yes? Well, no, unfortunately
not. Take a look at the bytecode of Main0
:
public final class Main0 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER Constant pool: #1 = Methodref #7.#16 // java/lang/Object."<init>":()V #2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream; #3 = Class #19 // Constants #4 = String #20 // com.io7m.name ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #5 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #6 = Class #23 // Main0 #7 = Class #24 // java/lang/Object ... #19 = Utf8 Constants #20 = Utf8 com.io7m.name ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V ... { public Main0(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String com.io7m.name ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 6: 0 line 7: 8 }
You can see that the value of the NAME
constant has been inlined
and inserted into the Main0
class's constant pool directly. This
means that if you change the value of NAME
in the Constants
class at a later date, the Main0
class will need to be recompiled
in order to see the change.
What can be done instead? Wrap the constant in a static method:
public final class ConstantsWrapped { private static final String NAME = "com.io7m.name"; public static final String name() { return NAME; } private ConstantsWrapped() { } }
Call the method instead of referring to the constant directly:
public final class Main1 { public static void main( final String args[]) { System.out.println(ConstantsWrapped.name()); } }
Now the resulting bytecode is:
public final class Main1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #18.#19 // ConstantsWrapped.name:()Ljava/lang/String; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #4 = Methodref #20.#21 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #22 // Main1 #6 = Class #23 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Main1.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #24 // java/lang/System #17 = NameAndType #25:#26 // out:Ljava/io/PrintStream; #18 = Class #27 // ConstantsWrapped #19 = NameAndType #28:#29 // name:()Ljava/lang/String; #20 = Class #30 // java/io/PrintStream #21 = NameAndType #31:#32 // println:(Ljava/lang/String;)V #22 = Utf8 Main1 #23 = Utf8 java/lang/Object #24 = Utf8 java/lang/System #25 = Utf8 out #26 = Utf8 Ljava/io/PrintStream; #27 = Utf8 ConstantsWrapped #28 = Utf8 name #29 = Utf8 ()Ljava/lang/String; #30 = Utf8 java/io/PrintStream #31 = Utf8 println #32 = Utf8 (Ljava/lang/String;)V { public Main1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: invokestatic #3 // Method ConstantsWrapped.name:()Ljava/lang/String; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 9: return LineNumberTable: line 6: 0 line 7: 9 }
This effectively solves the issue. The ldc
opcode is changed to an
invokestatic
opcode, at no point does the string com.io7m.name
appear directly in the Main1
class, and the value of the constant
can be changed at a later date without breaking binary compatibility.
Additionally, the JIT compiler will inline the invokestatic
call
at run-time, meaning that there's no performance degradation over
using the constant directly.
When I set up the initial FreeBSD install
to host io7m.com, I didn't realize how trivial
it was to use ZFS as the root
partition. Having used this option several times since, I now wish I had
done this for the io7m
VPS. I might spin up
a new VPS over the next few days with a ZFS root partition, copy the
configuration data over to the new VPS, and then reconfigure DNS to
point to the new system. If there's a mysterious outage, this will
be the reason why.
Whilst working on smf, I ran into an issue when resampling 32-bit floating point mesh data to 16-bit floating point format. The issue turned out to be poor handling of subnormal values by my ieee754b16 package. I went looking for better implementations to borrow and found a nice paper by Jeroen van der Zijp called Fast Half Float Conversions. It uses precomputed lookup tables to perform conversions and appears to be drastically more accurate than my manual process (the mathematics of which I've almost entirely forgotten).
I decided to put together a simple C99 implementation in order to see
how the code worked but am having some strange issues with some very
specific values. My test suite basically tries to prove that packing a
double
value and then unpacking it should be an approximate identity
operation. Essentially, ∀x. unpack(pack(x)) ≈ x
. Unfortunately,
some very specific values are failing. For some reason, my
implementation yields these results:
unpack(pack(2048.0)) → 2048.0 unpack(pack(2047.0)) → -0.0 unpack(pack(2046.0)) → 2046.0
unpack(pack(16375.0)) → 16368.0 unpack(pack(16376.0)) → 0.0
All of the other values in the range [-32000, 32000]
appear to be
correct. The unusual 16375.0 → 16368.0
result is expected; the conversion is
necessarily a lossy procedure and 16368.0
is simply the nearest
representable value when converting down to 16-bits. However, the
0.0
values are utterly wrong. This suggests that there's an issue in
implementation that's almost certainly caused by a mistake generating
the conversion tables. It seems that packing is correct, but unpacking
isn't. I've gone over the code several times, even going so far as
to implement it twice in two different languages and have gotten
the same results every time. I've spoken to Jeroen and he showed me
some results from his own implementation and test suite that show
that the above isn't a problem with the algorithm. So, assuming that
I haven't managed to screw up the same implementation after some
five clean-room attempts, there may be a transcription mistake in
the paper. I'm waiting to hear more from Jeroen.