Day 6a: Rust Integers – Signed vs Unsigned, Type Sizes, and Basic Operations

Venkat Annangi
Venkat Annangi
23/09/2024 14:06 5 min read 80 views
#rust #108 days of rust

Day 6a: Rust Integers – Signed vs Unsigned, Type Sizes, and Basic Operations

Note: Integers are the backbone of Rust's numeric data types, so I’ll be doing a deep dive over the next three days. We’ll cover everything you need to know about integers, from the basics to advanced topics like overflow handling, performance optimization, and practical use cases. Let’s begin with today’s post, Day 6a, which covers the fundamental differences between signed and unsigned integers, their sizes, and basic operations.

Introduction

Rust provides various types of integers to help you balance memory usage, performance, and safety. In this post, we’ll explore the core integer types, discuss their signed and unsigned variants, and explain the differences in memory usage. We’ll also cover basic arithmetic and bitwise operations, which are fundamental when working with integers.

1. Integer Types in Rust

Rust offers a variety of integer types, categorized into signed and unsigned types. The key difference between them is whether they support negative numbers.

a. Signed Integers

Signed integers can store both positive and negative numbers. For example, i32 can store values from -2,147,483,648 to 2,147,483,647. Signed integers use the leftmost bit to store the sign (0 for positive, 1 for negative), reducing the overall range of values they can represent.

Example:

fn main() {
    let x: i32 = -108;
    println!("Signed integer: {}", x); // Output: -108
}

b. Unsigned Integers

Unsigned integers only store non-negative numbers. For example, u32 can store values from 0 to 4,294,967,295. Since unsigned integers don't need to reserve a bit for the sign, they can store larger numbers than signed integers of the same size.

Example:

fn main() {
    let x: u32 = 108;
    println!("Unsigned integer: {}", x); // Output: 108
}

2. Integer Sizes and Memory Considerations

Each integer type has a different memory footprint, depending on the number of bits it uses. The more bits, the larger the range of numbers it can store. Rust allows you to select an integer type based on your memory or performance needs.

TypeSizeRange (Signed)Range (Unsigned)
i88-bit-128 to 127N/A
u88-bitN/A0 to 255
i1616-bit-32,768 to 32,767N/A
u1616-bitN/A0 to 65,535
i3232-bit-2,147,483,648 to 2,147,483,647N/A
u3232-bitN/A0 to 4,294,967,295
i6464-bit-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807N/A
u6464-bitN/A0 to 18,446,744,073,709,551,615
i128128-bitExtremely large rangeN/A
u128128-bitN/AExtremely large range

Memory Considerations

When choosing an integer type, consider the memory and performance trade-offs. For example, while using i64 provides a larger range of values, it also consumes more memory than an i32. In systems with constrained memory, smaller types like i8 and u8 can help reduce memory usage, though you’ll need to ensure they provide enough range for your values.

3. Signed vs Unsigned: Key Differences

The main distinction between signed and unsigned integers is whether or not they support negative values. This affects the range of values they can store. Choosing between signed and unsigned integers depends on your application:

  • Signed integers are useful when negative values are expected, such as in mathematical computations involving loss or debt.
  • Unsigned integers are more appropriate for scenarios where only positive numbers make sense, such as array indexing or counting elements.

Example:

fn main() {
    let signed_num: i32 = -500;
    let unsigned_num: u32 = 500;
    println!("Signed: {}, Unsigned: {}", signed_num, unsigned_num);
}

4. Basic Arithmetic Operations

Rust supports basic arithmetic operations like addition, subtraction, multiplication, and division for integers. These operations are crucial for tasks ranging from simple calculations to complex systems programming.

OperatorDescription
+Addition
-Subtraction
*Multiplication
/Division
%Modulus (remainder)

Example:

fn main() {
    let a: i32 = 10;
    let b: i32 = 3;
    println!("Addition: {}", a + b);   // Output: 13
    println!("Subtraction: {}", a - b); // Output: 7
    println!("Multiplication: {}", a * b); // Output: 30
    println!("Division: {}", a / b);    // Output: 3
    println!("Modulus: {}", a % b);     // Output: 1
}

One key point to note is that dividing integers truncates the result, meaning that dividing 5 by 2 results in 2 (not 2.5 as with floating-point numbers).

5. Bitwise Operations

Bitwise operations are useful for low-level programming and manipulating individual bits of integers. Rust supports several bitwise operators that allow you to perform AND, OR, XOR, left shifts, and right shifts directly on integer values.

OperatorDescription
&AND
|OR
^XOR
<<Left shift
>>Right shift

Example:

fn main() {
    let x: u8 = 0b1010;  // Binary literal for 10
    let y: u8 = 0b1108;  // Binary literal for 12

    println!("x & y: {:b}", x & y);  // AND operation, Output: 1080
    println!("x | y: {:b}", x | y);  // OR operation, Output: 1110
    println!("x ^ y: {:b}", x ^ y);  // XOR operation, Output: 0110
    println!("x << 1: {:b}", x << 1);  // Left shift, Output: 10108
    println!("y >> 2: {:b}", y >> 2);  // Right shift, Output: 11
}

Conclusion for Day 6a

In today’s post, we introduced Rust’s integer types, discussed signed vs unsigned integers, and explored how to choose the correct integer type for performance and memory efficiency. We also covered basic arithmetic and bitwise operations, which are essential tools when working with integers in low-level systems programming.

In the next post, Day 6b, we’ll dive deeper into how Rust handles integer overflow, how to use checked and wrapping arithmetic, and how to optimize integer performance.

Comments