Try using a multi- column index , but with the reverse order in the second column:
CREATE INDEX index_ips_begin_end_ip_num ON ips (begin_ip_num, end_ip_num DESC);
An order is basically irrelevant for a single-column index, as it can be scanned back almost as fast. But this is important for multi-column indexes.
Using the index I suggest, Postgres can scan the first column and find the address where the rest of the index fulfills the first condition. Then, for each value of the first column, it can return all rows that satisfy the second condition, until the first is completed. Then move on to the next value of the first column, etc.
This is still not very efficient, and Postgres can scan the first column of the index faster and filter for the second. A lot depends on your data distribution.
What really helps here is the GiST index for the int8range column , available with PostgreSQL 9.2.
If this is not the case, you can check out this close answer on dba.SE with a rather complicated mode with partial indexes. Advanced material, but it provides excellent performance.
In any case, CLUSTER using a multi-column index at the top can help performance:
CLUSTER ips USING index_ips_begin_end_ip_num
In this way, candidates fulfilling your first condition are packed on the same or adjacent data pages. It can help a lot in performance if you have many rows per value of the first column. Otherwise, it is hardly effective.
In addition, does auto-vacuum work or did you run ANALYZE on the table? You need current statistics for Postgres to select the appropriate query plans.