29

This Dart official video states that Dart's so-called "sound null safety" is better than Kotlin's null safety design, because it can optimise the code based on whether a variable is declared nullable, and other languages (I assume this refers to languages including Kotlin) have to do runtime checks to ensure null safety.

So, what extra optimization does Dart do?

How does it interoperate with legacy codebases that are not null-safe (written before null safety) while ensuring null safety?

creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
Yihao Gao
  • 535
  • 7
  • 11
  • 1
    I can answer for Kotlin. It almost 100% supports Java libraries. Kotlin can't assert nullability of such libraries as java bytecode doesn't distinguish between nullable and non-nullable type. So Kotlin created a platform type. Nullability of platform these types can only be checked at runtime. – Mangat Rai Modi Dec 03 '20 at 08:40
  • 1
    I can recommend reading the more detailed description about the null-safety feature: https://dart.dev/null-safety/understanding-null-safety – julemand101 Dec 03 '20 at 09:00
  • @MangatRaiModi However, that only applies to code from Java or other JVM languages (and not annotated for nullability); within Kotlin code, the compiler doesn't need any runtime checks. – gidds Dec 03 '20 at 10:59
  • @gidds But it does anyway, even in a pure Kotlin app. – Tenfour04 Dec 03 '20 at 13:13
  • @Tenfour04 Only for values that could be passed directly from non-Kotlin code; the compiler does not add checks where it can see that they're not needed.  For example, decompiling this class: `class Test(val s: String) { fun printLength() { println(s.length) } }` shows a null check in the constructor, but none in the function. – gidds Dec 10 '20 at 11:44

2 Answers2

15

Dart sound null safety

So, what extra optimization does Dart do?

The benefit of sound null safety in Dart is that the compiler can make use of a non-nullable type, which can elimate null checks. Therefore, the compiler will generate fewer instructions (which results in smaller binaries and faster runtime).

Example

Take the following function:

int getAge(Animal a) {
  return a.age;
}

These are the instructions the compiler generated before sound null safety:

compiled getAge function without null safety

As you can see, there are explicit instructions for checking null in the compiled code.

And this is what the same function looks like compiled with sound null safety:

compiled getAge function with null safety

Now, these additional instructions are no longer needed.

Note that the actual instructions generated starting with Dart 2.12 for the example function are the following (there were further optimizations):

compiled getAge function Dart 2.12 sound null safety

See Dart and the performance benefits of sound types by Vijay Menon (Engineering Lead, Dart) for reference.

Interoperability with legacy Dart code bases

How does it interoperate with legacy codebases that are not null-safe (written before null safety) while ensuring null safety?

It does not.

Well, that is not the whole truth. If you want to use Dart >=2.12.0 with any codebase that was written before Dart 2.12 (and with that before null safety), you cannot make use of sound null safety. You can, however, interoperate with these legacy codebases by passing a compiler flag that disables sound null safety. That would be --no-sound-null-safety (see my previous answer for more details).

This means that all benefits of sound null safety are lost when interacting with legacy codebases. This is also why the Dart team encourages all package authors to migrate their code to null safety.

Comparison to Kotlin

Kotlin simply does not have the additional compiler optimizations that Dart achieves with unboxed values thanks to sound null safety.

Keep in mind that Kotlin always allows interoperability with Java, which does not have any concept of null safety. I would imagine that this is a reason why Kotlin will never be able to have sound null safety in the same way that Dart code that interoperates with legacy codebases does not. That is as long as Kotlin code is compiled for the JVM with Java interoperability.

NNBD

If we are not concerned about the compiled code but only about the developer experience, Kotlin and Dart handle null safety identically. That is both languages are non-nullable by default (NNBD).

This means that when writing code in Dart 2.12+ or in Kotlin, all types are assumed to be non-nullable unless you explicitly mark them as nullable.
The only way to get a null pointer exception is by programmer error in both languages, i.e. using the bang operator ! in Dart and double bang operator in Kotlin !!, i.e. a not-null assertion by the developer.

Note that when using null assertions, additional runtime checks have to be added to the compiled code to preserve soundness in Dart. These checks always exists for Kotlin code compiled for the JVM as it is not sound to begin with.

This can also happen when interoperating with Java code when using Kotlin or interoperating with legacy code when using Dart.

There are some more edge cases, see Null safety in Kotlin and Understanding null safety in Dart for reference.

creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
  • Thank you for the detailed answer on the Dart side. Referencing the compiled assembly instructions are great. I'm not sure about the original poster of the question, but for me when I added the bounty, I'm especially interested in the details of the Kotlin side since I'm less familiar with that language. For example, what are some specific code examples of where Kotlin might throw a null error even though it has null safety? – Suragch Apr 21 '22 at 05:16
  • 1
    @Suragch I added an NNBD section to the end of my answer. – creativecreatorormaybenot Apr 22 '22 at 12:03
  • What about Kotlin/Native - can these types of checks be ommited there? – Vapid Apr 22 '22 at 13:03
  • Yes, now that you put it that way, I think my question is about developer experience. You say that the developer experience is identical. Do you mean that a developer won't experience an NPE in Kotlin precisely the same way they wouldn't in Dart? Your helpful link to the Null Safety in Kotlin page had a pretty long list of possible NPEs. It seems to me that some of these may be impossible in Dart due to sound null safety, but I'm missing a code example to prove that. I'm pushing the boundaries here because I'm really trying to understand this issue. – Suragch Apr 23 '22 at 01:35
  • @Suragch "Do you mean that a developer won't experience an NPE in Kotlin precisely the same way they wouldn't in Dart?" - exactly, that is the point of null safety in Kotlin. The common way it would still occur is via `!!` or via Java interop, which is analogous to `!` and legacy interop in Dart. – creativecreatorormaybenot Apr 23 '22 at 14:53
  • 1
    OK, I can accept that. Thank you for all the work you put in this answer. – Suragch Apr 25 '22 at 21:47
0

So, what extra optimization does Dart do?

The most basic kind of optimization is that when performing calculations on numeric types, compiler can treat them (internally) as primitive types of non-reference types (unboxed values).

Why is that?

Because they cannot be null and, therefore, it is not necessary to use them as data of referenced types (boxed values).

Why is that?

Because null is represented in Dart as a null constant reference.
If there is no need to refer to this constant, then why not use value types instead of reference type? At least in the generated code, which can be optimized already at compile time.

All this thanks to the so-called "strong mode".
The strong mode in conjunction with non-nullable types allows you to optimize the code already at the compilation stage, which is very important for modes such as AOT, which do not allow code to be optimized at runtime, because it is in the RE (read and execute) mode.

How does it interoperate with legacy codebases that null-safety is not supported while ensuring null safety?

It seems to me that you should ask this as a separate question.

Second Person Shooter
  • 14,188
  • 21
  • 90
  • 165
mezoni
  • 10,684
  • 4
  • 32
  • 54
  • the kotlin compiler actually does the same optimizations. so sound nullability is just a marketing thing. – deviant Mar 15 '21 at 13:35
  • 2
    @deviant no its not "just a marketing thing", there is a fundemental difference between sound and unsound null safety. Kotlins design choices mean it does NOT have it, Dart does. Please do not make ill informed comments. – Maks Mar 21 '21 at 23:58
  • If I'm not mistaken, if the variable is not nullable, or is deduced to be not nullable, Kotlin (JVM) doesn't box primitive value as an object either. And whether a variable is nullable is determined based on the code context at compile-time, with the help of so-called smart casting. – Yihao Gao Mar 23 '21 at 10:37
  • 1
    @YihaoGao Java VM does not support non-nullable by default. This means that everything in Kotlin is just syntactic sugar. That is, they seem to be there, but not at the level of execution time. – mezoni Mar 23 '21 at 11:22
  • @mezoni Kotlin has its own compiler with its own optimizations and a different standard library. Stating that Kotlin is just syntactic sugar is like saying all languages implemented on top of LLVM are syntactic sugar for asm. Also, you're wrong: https://docs.oracle.com/javase/specs/jvms/se6/html/Concepts.doc.html#15858 – Tin Svagelj Feb 19 '22 at 06:32
  • @TinSvagelj The LLVM and the Java Development Kit (JDK) is not the same things. The Java Development Kit (JDK) is a distribution of `Java Technology`. It implements the `Java Language Specification` and the `Java Virtual Machine` Specification and provides the Standard Edition of the `Java Application` Programming Interface (API). But the LLVM Project is a collection of modular and reusable compiler and toolchain technologies. LLMM is not even a programming language. Where can there be syntactic sugar? – mezoni Feb 19 '22 at 11:28
  • @TinSvagelj It would be interesting to know what were you thinking when you put your thoughts in writing? Or do you think the Java Development Kit is the same as LLVM? Personally, I do not think so, and I cannot be convinced of this without arguments. Kotlin borrows and uses everything from Java. Everything but syntax and grammar. LLVM does not imply any syntax, grammar, or API for user programs. And how can you afford to make me believe that they are the same? – mezoni Feb 19 '22 at 11:41
  • @TinSvagelj Kotlin is a high-level language built around and completely dependent on the environment of another high-level language. Assembler language is the lowest level language. It is a human readable machine language. How does Kotlin's implementation (and borrowings) compare to other languages that compile to machine code? And what kind of syntactic sugar were you writing about if there are so many varieties of Assembler, but none of the high-level languages directly depends on them (unlike Kotlin, which depends on the Java DDK implementation)? – mezoni Feb 19 '22 at 12:00
  • @mezoni We're off-topic, so I'll keep it brief. I haven't mentioned JDK once in my comment, only JVM. And I didn't compare JVM with LLVM, I only said stating Java and Kotlin are the same bc of JVM is like saying Rust and C++ are same because they are built on top of LLVM. Kotlin _only_ shares the JVM with Java, and as someone who actually used it, I know their stack is different than Java's. Kotlin bytecode is _intentionally_ kept compatible with Java for interoperability. You're attacking the strawman here - JVM _has_ a concept of non-nullable primitives and Kotlin uses them. – Tin Svagelj Feb 19 '22 at 21:32