CARVIEW |
Optimizing Memory Usage with NumPy Arrays
Learn how to effectively optimize memory usage using NumPy arrays in Python.

Image by Wesley Tingey | Unsplash
Memory optimization is very important when working on a data science and machine learning project. Before digging deeper into this article, let's build muscle memory by first understanding what memory optimization means and how we can effectively use NumPy for this task.
Managing and effectively distributing the computer memory resources so as to minimize memory usage while making sure that the computer system performance is at its peak is known as memory optimization.
When writing code, you need to use the appropriate data structures to maximize memory efficiency.This is because some data types consume less memory, and some consume more. You must also consider memory duplication and make sure to avoid it at all cost while freeing unused memory regularly.
NumPy is very efficient in memory unlike Python lists. NumPy stores data in a with a contiguous memory block while Python lists stores element as separate objects.
NumPy arrays have fixed data types, meaning all elements occupy the same amount of memory. This further reduces memory usage compared to Python lists, where each element's size can vary. This makes NumPy much more memory-efficient when handling large datasets.
How NumPy Arrays Store Data in Contiguous Blocks of Memory
NumPy arrays store their elements in contiguous (adjacent) blocks of memory, meaning that all the components are packed tightly together. This layout allows fast access and efficient operations on the array, as memory lookups are minimized.
Since NumPy arrays are homogeneous, meaning all elements have the same data type, the memory space required for each element is identical. NumPy only needs to store the size of the array, the shape (i.e., dimensions), and the data type. Then it allows a direct access to elements via their index positions without following pointers. As a result, operations on NumPy arrays are much faster and require less memory overhead compared to Python lists.
Memory Layout in NumPy
There are two memory layouts in NumPy, namely, C-order and Fortran-order.
- C-order, also known as row-major order: When iterating over the items in C-order, the array's final index changes the quickest. This indicates that data is kept in memory row by row, with each row being stored in a sequential manner. In NumPy, this is the default memory layout that works well for row-wise traversal operations
- Column-major order, or Fortran-order: Since the first index varies the quickest in Fortran-order, items are kept column by column. When interacting with systems that employ array storage in the Fortran manner or when doing numerous column-wise operations, this arrangement is helpful
The choice between C-order and Fortran-order can impact both the performance and memory access patterns of NumPy arrays.
Optimizing Memory Usage
In this section, we will cover the different methods and ways to optimize memory usage using NumPy arrays. Some of these methods include choosing the right data type, using views instead of copies, using broadcasting efficiently, reducing array size with np.squeeze
and np.compress
, and memory mapping with np.memmap
Choosing the Right Data Types
Choosing the right data type (dtype) for your NumPy arrays is one of the main strategies to minimize memory utilization. The data type you select will determine the memory footprint of an array, since NumPy arrays are homogeneous, meaning that every element in an array has the same dtype. You can save memory by utilizing smaller data types that are still within the range of your data.
import numpy as np
# Using default float64 (8 bytes per element)
array_float64 = np.array([1.5, 2.5, 3.5], dtype=np.float64)
print(f"Memory size of float64: {array_float64.nbytes} bytes")
# Using float32 (4 bytes per element)
array_float32 = np.array([1.5, 2.5, 3.5], dtype=np.float32)
print(f"Memory size of float32: {array_float32.nbytes} bytes")
# Using int8 (1 byte per element)
array_int8 = np.array([1, 2, 3], dtype=np.int8)
print(f"Memory size of int8: {array_int8.nbytes} bytes")
Code explanation:
- The
float64
dtype consumes 8 bytes (64 bits) per element, which is double the memory consumption of float32 (4 bytes) - The
int8
dtype uses just 1 byte per element. This is ideal when dealing with small integer values that fit within this range, reducing memory consumption significantly
Using Views Instead of Copies
A view in NumPy refers to a new array object that refers to the same data as the original array. This saves memory because no new data is created. On the other hand, a copy is a new array object with its own separate copy of the data. Modifying a copy will not affect the original array, as it occupies its own memory space.
# Original array
original_array = np.array([1, 2, 3, 4, 5])
# Creating a view (shares the same memory as the original array)
view_array = original_array[1:4]
view_array[0] = 10Â # Modifies original_array as well
# Creating a copy (allocates new memory)
copy_array = original_array[1:4].copy()
copy_array[0] = 20Â # Does not modify original_array
Code explanation:
- When you modify
view_array
, the change is reflected in original_array, as they share the same memory - However, modifying
copy_array
doesn’t affectoriginal_array
because a copy creates a completely new array in memory, leading to higher memory usage
Efficient Use of Broadcasting
Broadcasting in NumPy is a powerful feature that allows arrays of different shapes to be used in arithmetic operations without explicitly reshaping them. It will enable NumPy to perform operations on arrays of different shapes without creating large temporary arrays, which saves memory by reusing existing data during operations instead of expanding arrays.
Broadcasting works basically by automatically expanding smaller arrays along their dimensions to match the shape of larger arrays in an operation. This eliminates the need to manually reshape arrays or create unnecessary temporary arrays, saving memory.
# Arrays of different shapes
array = np.array([1, 2, 3])
scalar = 2
# Broadcasting scalar to perform multiplication
result = array * scalar
print(result)Â # Output: [2, 4, 6]
Code explanation:
- In this example, the scalar 2 is broadcasted so as to match the shape of the array, and the operation is performed without allocating extra memory for a new array
- Broadcasting is efficient because it avoids creating temporary arrays that could increase memory usage
Reducing Array Size with np.squeeze and np.compress
NumPy has operations such as np.squeeze
and np.compress
, which help minimize array sizes by eliminating unnecessary dimensions or filtering certain data.
# Array with unnecessary dimensions
array_with_extra_dims = np.array([[[1], [2], [3]]])
# Remove the extra dimensions
squeezed_array = np.squeeze(array_with_extra_dims)
print(squeezed_array.shape)Â # Output: (3,)
# Original array
data = np.array([1, 2, 3, 4, 5])
# Use np.compress to filter data
filtered_data = np.compress([0, 1, 0, 1, 0], data)
print(filtered_data)Â # Output: [2, 4]
Code explanation:
np.squeeze
removes dimensions of size 1, which simplifies the shape of the array and saves memory by reducing complexity in memory allocationnp.compress
filters an array based on a condition, creating a smaller array that reduces memory usage by discarding unnecessary elements
Memory Mapping with np.memmap
Memory mapping (np.memmap
) allows you to work with large datasets that don’t fit into memory by storing data on disk and accessing only the necessary portions.
# Create a large array on disk using memory mapping
data = np.memmap('large_data.dat', dtype=np.float32, mode='w+', shape=(1000000,))
# Modify a portion of the array in memory
data[5000:5010] = np.arange(10)
# Flush changes back to disk
data.flush()
Code explanation:
np.memmap
creates a memory-mapped array that accesses data from disk rather than storing it all in memory. You will find this useful when handling datasets that exceed your system's memory limits- You can modify portions of the array, and the changes are written back to the file on disk, saving memory
Conclusion
In conclusion, in this article we have been able to learn how to optimize memory usage using NumPy arrays. If you are conveniently leverage the methods highlighted in this article such as choosing the right data types, using views instead of copies, and taking advantage of broadcasting, you can significantly reduce memory consumption without sacrificing performance.
Shittu Olumide is a software engineer and technical writer passionate about leveraging cutting-edge technologies to craft compelling narratives, with a keen eye for detail and a knack for simplifying complex concepts. You can also find Shittu on Twitter.
- Masked Arrays in NumPy to Handle Missing Data
- How to Apply Padding to Arrays with NumPy
- How to Compute the Cross-Correlation Between Two NumPy Arrays
- Visualizing Data Directly from Numpy Arrays
- How to Perform Memory-Efficient Operations on Large Datasets with Pandas
- Processing a Directory of CSVs Too Big for Memory with Dask
Latest Posts
- We Benchmarked DuckDB, SQLite, and Pandas on 1M Rows: Here’s What Happened
- Prompt Engineering Templates That Work: 7 Copy-Paste Recipes for LLMs
- A Complete Guide to Seaborn
- 10 Command-Line Tools Every Data Scientist Should Know
- How I Actually Use Statistics as a Data Scientist
- The Lazy Data Scientist’s Guide to Exploratory Data Analysis
Top Posts |
---|
- We Benchmarked DuckDB, SQLite, and Pandas on 1M Rows: Here’s What Happened
- How I Actually Use Statistics as a Data Scientist
- The Lazy Data Scientist’s Guide to Exploratory Data Analysis
- Prompt Engineering Templates That Work: 7 Copy-Paste Recipes for LLMs
- 10 Command-Line Tools Every Data Scientist Should Know
- A Gentle Introduction to TypeScript for Python Programmers
- 5 Fun AI Agent Projects for Absolute Beginners
- A Complete Guide to Seaborn
- From Excel to Python: 7 Steps Analysts Can Take Today
- A Gentle Introduction to MCP Servers and Clients