For years, developers have used unsafe code when performance mattered.

Direct pointer access can reduce some checks and give developers more control over memory operations. In very specific workloads, that control can still be useful.

However, modern .NET has become much better at optimizing ordinary C# code. The JIT compiler can now recognize more patterns, remove redundant bounds checks, improve loop optimization, and generate better machine code without requiring developers to use pointers.

.NET 11 continues this work. Microsoft documents improvements in areas such as bounds-check elimination, loop analysis, assertion propagation, devirtualization, SIMD code generation, and other JIT optimizations.

That does not mean unsafe code is obsolete. It means the decision to use it should be based on measurement rather than the assumption that pointer-based code is automatically faster.

Why Safe C# Can Be Fast

Consider a simple array loop:

static int Sum(int[] values)
{
    int total = 0;

    for (int i = 0; i < values.Length; i++)
    {
        total += values[i];
    }

    return total;
}

At the C# level, values[i] looks like it needs a bounds check on every iteration.

The JIT, however, can reason about the loop.

It already knows that:

i >= 0

and:

i < values.Length

Therefore, the bounds check may be unnecessary.

Modern .NET documentation specifically describes JIT optimizations that eliminate bounds checks when the compiler can prove an access is safe. .NET 11 expands the situations where this can happen.

This is one reason developers should not automatically replace safe array access with pointers.

What Changed in .NET 11?

.NET 11 contains a broad set of JIT improvements.

Some of the areas relevant to performance-sensitive C# code include:

The JIT sits between C# code and the CPU:

C# Source
    |
    v
IL
    |
    v
.NET JIT
    |
    v
Native Machine Code
    |
    v
CPU

This means an improvement in the JIT can make existing C# code faster without requiring developers to rewrite that code.

Microsoft's .NET 11 performance work includes several such JIT changes.

Safe Array Access vs Unsafe Pointer Access

Consider a safe implementation:

static long SumSafe(int[] values)
{
    long total = 0;

    for (int i = 0; i < values.Length; i++)
    {
        total += values[i];
    }

    return total;
}

An unsafe implementation could use pointers:

unsafe static long SumUnsafe(int[] values)
{
    long total = 0;

    fixed (int* ptr = values)
    {
        for (int i = 0; i < values.Length; i++)
        {
            total += ptr[i];
        }
    }

    return total;
}

The second version gives the developer direct memory access.

But that does not automatically make it faster.

The safe version gives the runtime an opportunity to optimize the loop while preserving the normal memory-safety guarantees of C#.

The correct question is therefore not:

Is unsafe code faster?

It is:

Does unsafe code make this particular hot path measurably faster?

Bounds Checks Are Not Always a Performance Problem

A common misconception is that array bounds checks are always expensive.

Consider:

for (int i = 0; i < values.Length; i++)
{
    result += values[i];
}

This is a pattern the JIT can understand very well.

The loop counter starts at zero and increases predictably. The loop condition is based directly on the array length.

That gives the JIT useful information about the valid index range.

.NET 11 improves bounds-check elimination for additional patterns, including cases involving calculations such as an index plus a constant and some index-from-end operations.

A More Complicated Loop

Now consider:

static int SumPairs(int[] values)
{
    int total = 0;

    for (int i = 0; i < values.Length - 1; i++)
    {
        total += values[i];
        total += values[i + 1];
    }

    return total;
}

The JIT has more information to reason about here.

The loop condition establishes that:

i < values.Length - 1

which also implies:

i + 1 < values.Length

.NET 11 improves the JIT's ability to recognize some such relationships and remove redundant checks.

This matters because performance-sensitive code does not always need to be rewritten using pointers to avoid every possible bounds check.

Safe Code Is Easier to Maintain

Compare these two approaches.

Safe C#:

static void Copy(int[] source, int[] destination)
{
    for (int i = 0; i < source.Length; i++)
    {
        destination[i] = source[i];
    }
}

Unsafe code:

unsafe static void Copy(int[] source, int[] destination)
{
    fixed (int* src = source)
    fixed (int* dst = destination)
    {
        for (int i = 0; i < source.Length; i++)
        {
            dst[i] = src[i];
        }
    }
}

The unsafe version introduces additional concerns:

If both implementations provide effectively the same production performance, the safe implementation is usually easier to understand and maintain.

This is why performance optimization should consider engineering cost as well as raw execution time.

Spans Are Another Important Option

For performance-sensitive code, Span<T> and ReadOnlySpan<T> provide a useful middle ground.

For example:

static long Sum(ReadOnlySpan<int> values)
{
    long total = 0;

    for (int i = 0; i < values.Length; i++)
    {
        total += values[i];
    }

    return total;
}

This code remains safe while providing an efficient abstraction for working with contiguous memory.

You can call it with an array:

int[] values = [1, 2, 3, 4, 5];

long total = Sum(values);

There is no need to expose pointers simply because the code is performance-sensitive.

When Unsafe Code Can Still Make Sense

There are legitimate scenarios for unsafe.

C# supports unsafe code for situations such as:

Microsoft's documentation also notes that unsafe code can be useful for direct memory access, but it introduces safety and stability risks.

For example:

unsafe
{
    int value = 42;
    int* pointer = &value;

    Console.WriteLine(*pointer);
}

This is valid C#, but using a pointer does not automatically provide a performance benefit.

The JIT Can Optimize More Than You Might Expect

The JIT does more than remove bounds checks.

It can perform transformations such as:

.NET 11 expands several of these capabilities.

For example, if the JIT can determine that a condition is always true or false based on information it has already established, it can simplify the generated code.

This means the C# source does not always represent the actual machine-level work performed by the application.

SIMD Can Reduce the Need for Manual Loops

For workloads involving large amounts of numeric data, SIMD can process multiple values with a single vector instruction.

.NET provides APIs such as:

Vector128<T>
Vector256<T>
Vector512<T>

and hardware intrinsics for more specialized scenarios.

The runtime can also determine whether hardware acceleration is available for supported vector types.

A simple example is:

static void Add(
    ReadOnlySpan<int> left,
    ReadOnlySpan<int> right,
    Span<int> result)
{
    for (int i = 0; i < left.Length; i++)
    {
        result[i] = left[i] + right[i];
    }
}

Before manually introducing pointers or processor-specific instructions, measure whether normal safe code already performs adequately.

For some workloads, higher-level APIs and JIT optimizations can provide the required performance without exposing unsafe memory operations.

Benchmark Before Changing the Code

A performance change should be measured.

BenchmarkDotNet is commonly used for controlled .NET microbenchmarks.

A simple benchmark structure can look like:

[MemoryDiagnoser]
public class SumBenchmarks
{
    private int[] values = null!;

    [GlobalSetup]
    public void Setup()
    {
        values = Enumerable.Range(1, 1_000_000).ToArray();
    }

    [Benchmark]
    public long Safe()
    {
        long total = 0;

        for (int i = 0; i < values.Length; i++)
        {
            total += values[i];
        }

        return total;
    }
}

If you are comparing safe and unsafe implementations, place both implementations in the same benchmark and test them under equivalent conditions.

Do not compare one implementation using Debug mode with another using Release mode.

Benchmark the Runtime Versions

If you want to understand the effect of .NET 11, benchmark the same application code against multiple runtimes.

For example:

Application code
       |
       +---- .NET 10
       |
       +---- .NET 11

Then compare:

Execution time
Allocated memory
Throughput

This is more useful than assuming that a particular language construct is faster based on theory alone.

Microsoft's own .NET 11 performance article contains runtime comparisons showing cases where the newer JIT produces fewer allocations or faster generated code. Those measurements are workload-specific and should not be treated as universal improvements for every application.

A Better Optimization Process

Instead of starting with unsafe, use a measurement-driven process.

Step 1 - Identify the Hot Path

Find the code that actually consumes meaningful CPU time.

Application
    |
    v
Profile
    |
    v
Hot method
    |
    v
Optimize

Do not optimize code simply because it looks low-level.

Step 2 - Write the Safe Version

Start with clear C#:

static int Sum(ReadOnlySpan<int> values)
{
    int result = 0;

    foreach (int value in values)
    {
        result += value;
    }

    return result;
}

Step 3 - Measure It

Use a representative workload.

Step 4 - Inspect the Results

Check:

Mean execution time
Allocated bytes
Throughput
CPU usage

Step 5 - Try a Different Implementation Only If Needed

If the safe implementation is already fast enough, stop.

If it is not, investigate:

This order usually produces a more maintainable result.

Safe Code Can Still Be Written Poorly

.NET 11 does not make every C# implementation automatically fast.

For example:

static long Sum(IEnumerable<int> values)
{
    long total = 0;

    foreach (var value in values)
    {
        total += value;
    }

    return total;
}

This accepts a broad abstraction.

The actual cost depends on what values represents.

A tight array or span loop provides the runtime with more direct information about contiguous memory.

This does not mean IEnumerable<T> is inherently bad.

It means the data structure and abstraction should match the workload.

Avoid Premature Unsafe Optimization

Consider a web API that spends most of its time waiting for:

Database
Network
External service

Changing a small CPU loop from safe C# to pointer-based code may have little effect on overall request latency.

The application's real bottleneck could be elsewhere.

A useful optimization question is:

What percentage of total execution time does this code represent?

If the answer is very small, making the code more complex may not provide a meaningful production benefit.

Comparison Table

Approach

Safety

Complexity

Potential Performance

Best Use

Normal C# arrays

High

Low

High

General application code

Span<T> / ReadOnlySpan<T>

High

Low to medium

High

Memory-oriented code

SIMD APIs

High

Medium

Very high for suitable workloads

Numeric processing

Hardware intrinsics

High at managed API level, hardware-specific

High

Very high for targeted workloads

Specialized CPU operations

unsafe pointers

Lower

High

Potentially very high

Specialized low-level scenarios

The table describes trade-offs, not guaranteed performance.

Actual results depend on the algorithm, data size, runtime, processor, and workload.

Common Mistakes

Assuming unsafe Is Automatically Faster

Pointer access does not guarantee faster generated code.

Benchmarking Tiny Examples Only

A microbenchmark may not represent application behavior.

Ignoring the JIT

Modern .NET can optimize patterns that appear expensive at the C# source level.

Optimizing Before Profiling

A faster method does not matter much if it is rarely executed.

Comparing Debug Builds

Performance measurements should generally use optimized Release builds.

Ignoring Allocations

Execution time is not the only metric.

An implementation that runs quickly but creates unnecessary allocations can increase garbage collection pressure.

Using Processor-Specific Code Without a Reason

Specialized instructions can improve performance but may increase complexity and portability concerns.

Best Practices

Start With Safe C#

Write readable code first.

static long Sum(ReadOnlySpan<int> values)
{
    long total = 0;

    for (int i = 0; i < values.Length; i++)
    {
        total += values[i];
    }

    return total;
}

Let the JIT Do Its Job

Do not manually remove a bounds check that the JIT can already eliminate.

Microsoft's unsafe-code guidance explicitly recommends checking whether the latest .NET runtime can already eliminate a bounds check before introducing unsafe code.

Profile Real Workloads

Use production-like data sizes and execution patterns.

Measure Allocations

Use allocation measurements alongside execution time.

Keep Unsafe Code Small

If unsafe code is necessary, isolate it behind a small, well-tested API.

Document Why It Exists

A future developer should understand why a pointer implementation is necessary.

For example:

// Unsafe implementation is retained because the measured
// workload requires this memory access pattern.

The comment should explain the measured reason rather than simply saying "for performance."

Advantages of Using Safe C# First

  1. Better memory safety

  2. Simpler maintenance

  3. Easier code review

  4. Lower debugging complexity

  5. Better portability

  6. More opportunities for the runtime to optimize automatically

Disadvantages and Trade-Offs

Safe code is not automatically optimal for every workload.

Potential limitations include:

  1. Some specialized workloads may require lower-level access.

  2. Hardware-specific optimization can require APIs beyond ordinary loops.

  3. Certain native integrations require unsafe code.

  4. The JIT cannot eliminate every possible runtime check.

  5. Specialized algorithms may still benefit from direct memory access.

The important point is that these limitations should be demonstrated through profiling and measurement.

Troubleshooting Performance Problems

Safe Code Is Still Slow

Check the algorithm first.

Changing:

O(n²)

to:

O(n)

can matter far more than removing a bounds check.

The JIT Does Not Remove a Check

Simplify the loop structure where practical and benchmark again.

For example, a predictable loop is easier for the JIT to reason about than complicated control flow.

.NET 11 Is Not Faster in Your Benchmark

That is possible.

Runtime improvements are workload-dependent.

Check:

Runtime version
CPU architecture
Build configuration
Input size
Benchmark methodology

Do not assume that every application will receive the same performance improvement.

Unsafe Code Is Only Slightly Faster

If the improvement is very small, consider whether the added complexity is justified.

Microsoft's guidance recommends measuring the real-world impact before retaining unsafe optimizations.

A Practical Decision Tree

Use this approach when optimizing a hot loop:

Is this code actually a bottleneck?
        |
       No
        |
        v
Keep the safe implementation

        Yes
        |
        v
Can the algorithm be improved?
        |
       Yes
        |
        v
Improve the algorithm

        No
        |
        v
Can safe C# be improved?
        |
       Yes
        |
        v
Use better data structures or APIs

        No
        |
        v
Benchmark specialized approaches
        |
        v
Does unsafe code provide a meaningful gain?
        |
   +----+----+
   |         |
  No        Yes
   |         |
   v         v
Keep safe   Isolate and
code        document unsafe code

Conclusion

.NET 11 continues to improve the ability of the JIT compiler to turn ordinary C# into efficient machine code.

Better bounds-check elimination, loop analysis, assertion propagation, devirtualization, SIMD improvements, and other runtime optimizations mean developers do not need to reach for unsafe code simply because a loop is performance-sensitive.

The practical lesson is simple: write safe C# first, measure it, profile the real bottleneck, and only introduce lower-level code when the data shows that it is necessary.

A good performance workflow is:

Write clear C#
      |
      v
Profile
      |
      v
Find the hot path
      |
      v
Benchmark
      |
      v
Improve safe code
      |
      v
Benchmark again
      |
      v
Use unsafe only when measurement justifies it

Modern .NET is increasingly capable of optimizing code without requiring developers to manually control every memory access. The result is that safe C# and high performance are no longer opposing goals for many workloads.