Helium Optimizations Explained

Helium Optimizations Explained

This document covers the optimization aspects of CM85 core MCUs with new Helium instructions, Helium vector processing, and low overhead branch extension. Formally known as Helium technology, the M-Profile Vector Extension (MVE) was first introduced in CM55 with Armv8.1-M architecture. It is also known as Helium MVE extensions.

More details on Helium optimizations can be found in the reference documents provided. Further, practical examples for Helium optimizations on Renesas RA8D1 MCU is shown later in the document.

Vectorized Code

Helium provides single instruction multiple data (SIMD) capability for Cortex-M CPUs. This means that a set of 128-bit registers can be used to hold, for example, 16 separate 8-bit values. Using the same example, a single instruction can operate on each 16 separate 8-bit values independently.

Helium instructions operate on vectors of elements of the same data type. These data types may be floating-point or integer. Integer elements may be signed or unsigned 8-, 16-, 32-bit, while floating-point elements may be single (32-bit) or half precision (16-bit).

The position of an element within a vector register is referred to as a lane. The instruction set is regular and orthogonal, and nearly all instructions perform the same operation in all lanes. This means that most instructions have n-parallel operations where n is the number of lanes that the input vectors are divided into. This is shown in the figure below.

image-20250916-174209.png

Low Overhead Branch Handling

Repetitive code execution can be optimized by running instructions simultaneously and using special registers. A loop begins with a While Loop Start (WLS) or Do Loop Start (DLS) instruction and finishes with a LE (Loop End) instruction.

The loop start instruction copies the iteration count to the LR register. For WLS, it also performs a check to see whether the iteration count is zero and, if so, causes execution to branch to the end of the loop. DLS is used for a loop where there will always be at least one iteration and so does not need this step. The LE instruction checks LR to see whether another iteration is required and, if so, it branches back to the start.

Further optimizations are possible if the processor can store the start and end addresses of the loop along with the use of vectorized code. This allows the loop to execute only the instructions inside the loop in multiple lanes.

Tail Prediction

An operation performed on a single byte per loop can be vectorized by Helium. For example, a loop that copies 100 bytes can be performed at 16 bytes per iteration because a 128-bit vector can contain 16 bytes of data. The remining 4 bytes (100 - 96 = 4) are copied in a separate block of non-vectorized code at a tail loop. The first loop runs 6 times and copy 96 bytes (6 x 16 = 96) and the second non-vectorized loop runs 4 times to copy the remining bytes.

However, there are now 2 blocks of loops and the code size is increased. Tail prediction solves these problems and allows the execution of loops that process multiple data elements in one single vector loop that are not an exact multiple of the number of elements that fit in a vector. Helium does this by including the TP (Tail Prediction) suffix to the LE instruction creating a new LETP (Loop End with Tail Prediction) instruction.

On the last loop, if the number of remaining elements is less than the size of the vector, the appropriate number of elements at the end of the vector are disabled.

Examples with Renesas RA8D1 MCU

The rest of this document covers practical examples of SIMD vector processing, low overhead branch and tail prediction handling. Test projects are created for Renesas RA8D1 MCU using the following tools:
• e2studio 2024-04.1
• LLVM for Arm v18.1.3

Consider the following memory set and memory copy functions. These types of operations are good examples for vector optimizations. This is because the same orthogonal data type in memory (unsigned 8-bit integers this case) is either set or copied from one location to another.

image-20250916-175234.png

To fully enable and see Helium optimizations impact, “-O2” optimization level is used. Further, the build flag “-fno-builtin” is used to disable compiler using memset() and memcpy() library code and compiler attribute “noinline” is used to disable compiler inlining the code.

Enable Helium, Auto Vectorization, and Low Overhead Branch Execution

To enable Helium and auto vectorization, make sure to set up the project properties as below.
• CPU: Arm family = “cortex-m85”
• Optimization: Optimization Level = “-O2”

image-20250916-175631.png
image-20250916-175713.png

The assembly output of the code is given below with added comments.

// r0 = destination address // r1 = source address // r2 = loopcount cm85_mem_set: 020000fc: push {r7, lr} 020000fe: mov r7, sp 02000100: cmp r2, #0 // compare r2 to 0 02000102: it eq // if-then block with one conditional instruction 02000104: popeq {r7, pc} // if equal, pop stack and return 02000106: vdup.8 q0, r1 // if not, vector duplicate each element of q0 0200010a: dlstp.8 lr, r2 // do loop start with tail prediction and vectorization 0200010e: vstrb.8 q0, [r0], #16 // vector store q0 to address in r0, post increment 02000112: letp lr, 0x200010e // loop end tail prediction 02000116: pop {r7, pc} cm85_mem_copy: 02000118: push {r7, lr} 0200011a: mov r7, sp 0200011c: cmp r2, #0 // compare r2 to 0 0200011e: it eq // if then block with one conditional instruction 02000120: popeq {r7, pc} // if equal, pop stack and return 02000122: subs r3, r0, r1 // subtract dest address from src address (r3 = r0 – r1) 02000124: cmp r3, #15 // compare to 15 02000126: bhi.n 0x200013a // if higher, branch 02000128: dls lr, r2 // if not, do loop start(non-vectorized, lr = loop count 0200012c: ldrb.w r2, [r1], #1 // load r2 from address in r1, post increment 02000130: strb.w r2, [r0], #1 // store r2 to address in r0, post increment 02000134: le lr, 0x200012c // loop end 02000138: b.n 0x200014a // if end, branch 0200013a: dlstp.8 lr, r2 // do loop start with prediction and vectorization 0200013e: vldrb.u8 q0, [r1], #16 // vector load q0 from address in r1, post increment 02000142: vstrb.8 q0, [r0], #16 // vector store q0 to address in r0, post increment 16 02000146: letp lr, 0x200013e // loop end tail prediction 0200014a: pop {r7, pc}

The instruction dlstp.8 lr, r2 sets up a tail prediction loop, where register lr contains the number of elements to be processed, with its initial value coming from r2. The letp lr, 0x200013e instruction branches back to 0x200013e and decrements the number of elements processed in lr. Notice the vldrb and vstrb instructions. These are vectorized operations that load and store 16 bytes of data at each iteration.

The lane width of the operation performed is specified by the instruction (.8 or .32 for example) and the number of elements to calculate the correct number of loop iterations to perform. During the last loop, if the number of remaining elements is less than the vector length, the appropriate number of elements at the end of the vector are disabled. This means that all parts of the memory copy can be executed in parallel (vectorized), and the non-vector tail code can be eliminated. The remining elements are still processed. This makes for simpler (and smaller) code, as well as speeding things up.

Disable Helium

You can disable all Helium optimizations as shown below. To do this, you need to change the CPU settings as:
• CPU: Arm family = “Toolchain default”
• Other CPU options = “-mcpu=Cortex-M85+nomve”

image-20250916-180644.png

Below is the assembly output with added comments.

// r0 = destination address // r1 = source address // r2 = loopcount cm85_mem_set: 020000fc: push {r7, lr} 020000fe: mov r7, sp 02000100: wls lr, r2, 0x200010e // while loop start 02000104: subs r0, #1 // decrement r0 02000106: strb.w r1, [r0, #1]! // pre increment, store r1 to address in r0 0200010a: le lr, 0x2000106 // loop end 0200010e: pop {r7, pc} cm85_mem_copy: 02000110: push {r7, lr} 02000112: mov r7, sp 02000114: wls lr, r2, 0x2000128 // while loop start 02000118: subs r0, #1 // decrement r0 0200011a: subs r1, #1 // decrement r1 0200011c: ldrb.w r2, [r1, #1]! // pre increment, load r2 from address in r1 02000120: strb.w r2, [r0, #1]! // pre increment, store r2 to address in r0 02000124: le lr, 0x200011c // loop end 02000128: pop {r7, pc}

Both set and copy loops start with wls while loop start instruction and lr has the loop iteration count. The major difference from the Helium enable assembly code is that there are no vectorized instructions here. In both loops, each byte of the memory location is set or read from and copied to another memory location one byte at a time.

Benchmark Results

Table below shows the comparison of Helium enabled vs Helium disabled performance improvement results on RA8D1. Each test was run to set or copy 399,990 bytes.

 

Number of Cycles

 

 

Operation

Helium
Disabled

Helium
Enabled

% Reduction in Number of Cycles with Helium

Helium Improvement Factor

Memory Set

800,847

175,056

-78.1%

4.57

Memory Copy

6,399,870

1,250,011

-80.5%

5.12

A graphical representation of the same results is shown below.

image-20250916-181136.png

Summary

This example shows one aspect of Helium optimization factors: vectorization. However, Helium optimizations encompass several techniques.
• Helium enabled libraries
• Auto vectorization
• Helium intrinsics
• Assembly code

It begins with the target application. Your “Hello World” program will not have much gain from Helium optimizations. However, mathematical algorithms, DSP calculations, and AI applications are prime targets for Helium optimizations. Main reason for this is that these types of applications can be structured such a way to utilize SIMD capability with vectorized instructions.

Below we overview options for writing Helium enabled code.

Helium-enabled Libraries

When you compile for a Helium-enabled target, a library variant using Helium instructions is selected. When you compile for a target that does not support Helium, a library variant using standard Arm instructions is selected. This means that the same source code can easily be compiled for both Helium-enabled targets and non-Helium-enabled targets.

Examples of Helium-enabled libraries are CMSIS-DSP and CMSIS-NN. LLVM complier used in this example chooses the Helium-enabled CMSIS libraries if the CPU supports it.

Auto Vectorization

Auto-vectorization means allowing the compiler to automatically identify the areas of your code that would benefit from SIMD optimizations. The benefit of using auto-vectorization is that the programmer leaves everything to the compiler. However, compiler might fail to identify a particular part of code can be vectorized.

We have seen examples of auto vectorization in our example. By using “-O2” optimization level, LLVM compiler auto vectorized the set and copy examples.

Helium Intrinsics

Helium intrinsics are function calls that the compiler replaces with appropriate Helium instructions. Using Helium intrinsics gives you direct, low-level access to the exact Helium instructions in your C/C++ code.

The benefit of using intrinsics is that they provide almost as much control as writing assembly language, but leave details like register allocation to the compiler, so that developers can focus on the algorithms.

The disadvantage of using Helium intrinsics is that programming with intrinsics can be more complex than writing standard C/C++ code and requires the programmer to learn about the available Helium intrinsics.

Assembly Code

For very high performance, hand-coded Helium assembly code is an alternative approach for experienced programmers. You can use pure assembly code files, or you can use inline assembly code to embed assembler instructions in your C and C++ code.

The disadvantages are writing on assembly code can be very complex process and require detailed knowledge of the target hardware.

References

  1. Arm Helium Technology, M-Profile Extension (MVE) for Arm Cortex-M Processor, by Jon Marsh ISBN 978-1-911531-23-4

  2. Arm Cortex-M85 Process Devices, Generic User Guide, 101928_0101_07_en