Trước đây, bộ nhớ vốn đã từng là loại tài nguyên tính toán quý hiếm nhất và hầu như người sử dụng luôn cảm thấy thiếu bộ nhớ. Do đó, rất nhiều chương trình đã cố gắng tối ưu hóa nhằm tiết kiệm “quá mức” không gian lưu trữ dữ liệu mà không lường trước những hậu quả của việc này. “Sự cố năm 2000” thường được lấy làm ví dụ cho điều này: khi bộ nhớ thực sự khan hiếm, các chương trình đã bỏ đi hai byte để lưu trữ thông tin thế kỷ dẫn đến hậu quả nghiêm trọng trên phạm vi toàn cầu. Cho dù vấn đề không gian lưu trữ có phải là nguyên nhân thật sự của vấn đề này hay không thì những đoạn mã lệnh như vậy đơn giản chỉ phản ánh cách thức mà con người ghi ngày tháng trong cuộc sống thường nhật, trong đó phần thông tin thế kỷ thường bị lược bỏ, điều này thể hiện mối nguy hiểm cố hữu trong một sự tối ưu hóa thiển cận.
Ngày nay, vấn đề khan hiếm bộ nhớ đã được giải quyết. Bộ nhớ chính cũng như bộ nhớ phụ đã trở nên ngày càng rẻ một cách đáng ngạc nhiên. Do vậy, hướng tiếp cận đầu tiên đối với vấn đề tối ưu hóa không gian lưu trữ cũng giống như đốivới vấn đề tối ưu hóa tốc độ xử lý, đó là không cần quan tâm đến việc tối ưu hóa không gian!
Tuy nhiên, vẫn có một số trường hợp cần tối ưu hóa không gian lưu trữ. Nếu một chương trình quá lớn, không thể được lưu trữ toàn bộ trong bộ nhớ chính, một số phần chương trình sẽ được swapout ghi tạm thời trên đĩa. Điều này làm cho việc thực hiện chương trình không hiệu quả. Ta thường gặp điều này ở các phiên bản mới của phần mềm sử dụng quá nhiều bộ nhớ. Một thực tế đáng buồn là mỗi khi việc nâng cấp phần mềm thường đòi hỏi phải mua thêm bộ nhớ.
6.1. Tiến kiệm không gian bằng cách dùng kiểu dữ kiệu có kích thước nhỏnhất nhất
Bước đầu tiên trong việc tối ưu hóa không gian lưu trữ là thực hiện những thay đổi nhỏ để sử dụng bộ nhớ hiệu quả hơn, ví dụ như sử dụng các kiểu dữ liệu nhỏ nhất có thể được. Chúng ta có thể thay kiểu int (4 byte) bằng kiểu short
(2 byte) nếu phù hợp. Đây là kỹ thuật phổ biến để lưu trữ các tọa độ trong hệ thống đồ họa hai chiều, vì chỉ cần dùng 16 bit ta có thể quản lý bất kỳ tọa độ nào trên màn hình. Ta cũng có thể thay kiểu double (độ chính xác kép) bằng kiểu float (độ chính xác đơn). Vấn đề là độ chính xác của dữ liệu bị giảm đi bởi vì kiểu float thường lưu được 6 đến 7 chữ số thập phân.
Trong trường hợp này cũng như một số các trường hợp tương tự, ta cần thực hiện thêm những thay đổi khác trong mã nguồn chương trình, đáng chú ý nhất là các định dạng kiểu dữ liệu trong hàm printf và scanf.
Áp dụng tư tưởng của hướng tiếp cận này, chúng ta có thể nén nhiều bit thông tin vào trong một byte. Như vậy, mỗi byte sẽ mang nhiều ý nghĩa, nhiều thông tin hơn. Trên thực tế, ta không nên thao tác trên từng bit của C hay C++: vì không linh hoạt, có xu hướng làm tăng kích thước mã chương trình và không hiệu quả. Thay vào đó, ta nên xây dựng các hàm cho phép thao tác trên các bit trong từng từ hay mảng các từ sử dụng các toán tử Shift và toán tử mặt nạ. Ví dụ: Hàm dưới đây trả về nhóm gồm n bit liên tiếp từ vị trí thứ p của một từ x:
/* Hàm getbits: lấy n bit từ vị trí thứ p */ unsignedintgetbits(unsignedint x, int p, int n) {
return (x >> (p+1-n)) & ~(~0 << n); }
Nếu những hàm như vậy xử lý quá chậm, bạn có thể cải tiến bằng các kỹ thuật đã được mô tả trước đây trong chương này. Trong C++, ta có định nghĩa toán tử để truy xuất các bit giống như truy xuất mảng bằng chỉ số.
6.2. Không cần lưu trữ những gì có thể tính lại dễ dàng
Thường ít khi ta sử dụng kỹ thuật này. Những cải tiến chủ yếu thường đạt được khi sử dụng cấu trúc dữ liệu tốt hơn, đôi khi gắn liền với việc thay đổi thuật toán. Dưới đây là một ví dụ: Nhiều năm trước đây, để xử lý một ma trận quá lớn trên máy tính, một lập trình viên có thể cần phải khởi động lại máy, nạp
tối thiểu các dịch vụ của hệ điều hành để có đủ không gian lưu trữ ma trận. Tuy nhiên, trên thực tế, ma trận cần xử lý là ma trận thưa (tức là hầu hết các phần tử trong ma trận mang giá trị 0). Theo đánh giá, chỉ khoảng 5% phần tử trong ma trận có giá trị khác 0. Như vậy, chúng ta không cần phải lưu trữ hết toàn bộ ma trận mà chỉ cần lưu lại những phần tử khác 0 trong ma trận. Khi đó, thao tác truy xuất phần tử m[i][j] sẽ được thay thế bằng việc gọi hàm m(i, j). Có nhiều cách để lưu trữ dữ liệu. Cách đơn giản nhất là sử dụng mảng các con trỏ, mỗi con trỏ tương ứng với một dòng trong ma trận (trỏ đến vùng nhớ dùng để lưu trữ một dòng trong ma trận). Thông tin mỗi phần tử trên một dòng sẽ bao gồm chỉ số cột và giá trị của phần tử đó (chỉ lưu lại những phần tử khác 0). Cách lưu trữ này tốn nhiều không gian lưu trữ cho các phần tử khác 0 nhưng tổng kích thước không gian lưu trữ cho toàn bộ ma trận giảm đi đáng kể. Chi phí cho việc truy xuất các phần tử trong ma trận sẽ cao hơn so với cách lưu trữ thông thường, tuy nhiên, rõ ràng là lập trình viên không cần phải khởi động lại máy mỗi khi muốn thực hiện chương trình.
Chúng ta sử dụng cách tiếp cận này để giải quyết bài toán tương tự trong thời đại hiện nay. Một hệ thống thiết kế sóng vô tuyến cần biểu diễn dữ liệu địa hình và cường độ tín hiệu vô tuyến trên một vùng địa lý rất rộng (kích thước mỗi chiều của vùng lên đến từ 100 đến 200 Km) với độ phân giải 100m. Nếu lưu trữ thông tin dưới dạng mảng, chương trình sẽ chiếm quá nhiều bộ nhớ và sẽ xảy ra hiện tượng thrashing (các trang liên tục được swap in và swap out). Nhận xét thấy rằng giá trị địa hình và cường độ sóng vô tuyến thường không đổi như trong từng vùng tương đối lớn. Do đó, chúng ta có thể sử dụng cấu trúc phân cấp để biểu diễn từng vùng có cùng giá trị dữ liệu vào cùng một ô
Các biến thể khác nhau của kỹ thuật thường được sử dụng với các cách tổ chức lưu trữ và biểu diễn thông tin khác nhau. Tuy nhiên, tất cả các biến thể này để có cùng một ý tưởng: lưu trữ các giá trị chung hay các giá trị xuất hiện nhiều lần ở dạng gọn nhất, chấp nhận tốn nhiều thời gian và không gian hơn cho các giá trị còn lại (thiểu số).
Chương trình cần được tổ chức lại sao cho việc biểu diễn dữ liệu được gói gọn bên trong một lớp hay một nhóm hàm xử lý trên dữ liệu đặc biệt này. Điều này đảm bảo phần còn lại của chương trình không bị ảnh hưởng bởi việc thay đổi cách biểu diễn dữ liệu.
Thông thường, chúng ta nên lưu trữ thông tin ở dạng văn bản trong trường hợp có thể được thay vì luôn dùng biểu diễn dạng nhị phân. Văn bản có tính khả
chuyển, dễ đọc và dễ xử lý bằng các công cụ khác nhau, trong khi dữ liệu biểu diễn dạng nhị phân không có những ưu điểm này. Biểu diễn nhị phân thường được dùng để tăng tốc xử lý nhưng chúng ta nên cân nhắc khi sử dụng.
Bài toán tối ưu hóa không gian lưu trữ và thời gian xử lý thường có khuynh hướng ngược chiều nhau. Nếu tập trung nhằm tối ưu hóa không gian lưu trữ thường làm giảm tốc độ xử lý. Chúng ta thử xét một ứng dụng chuyển một ảnh có kích thước lớn từ một chương trình này sang chương trình khác. Ảnh được lưu trữ ở dạng đơn giản (PPM) thường có kích thước vài megabyte. Người ta nghĩ rằng nên nén ảnh lại (chẳng hạn chuyển sang định dạng GIF với kích thước ảnh chỉ còn khoảng 50KB) để truyền đi. Khi đó, thời gian để truyền ảnh GIF sẽ nhanh hơn thời gian truyền ảnh PPM. Tuy nhiên, nếu thời gian để nén và giải nén ảnh GIF cũng lâu tương đương với thời gian truyền ảnh nguyên thủy (dạng PPM) thì rõ ràng giải pháp cải tiến không có hiệu quả. Ngoài ra, mã nguồn chương trình cho việc thực hiện nén và giải nén ảnh dạng GIF lên đến hơn 500 dòng lệnh, trong khi chương trình xử lý ảnh PPM chỉ khoảng 10 dòng lệnh. Để phục vụ cho nhu cầu dễ bảo trì, giải pháp nén GIF tỏ ra không hiệu quả và chúng ta quyết định vẫn sử dụng chương trình xử lý ảnh PPM ban đầu. Dĩ nhiên là trong trường hợp việc truyền một ảnh nguyên thủy PPM qua mạng quá chậm thì giải pháp nén GIF sẽ trở nên hiệu quả hơn.